Aug 30, 2014

mv   *.local_tasks.yml   *.links.task.yml

When I upgraded to D8 alpha 14, the three tabs that I had implemented for the module disappeared. The paths still worked since the forms were accessible by using the address field of the browser.

A search on the core .yml files in D8 alpha 13 compared with alpha 14 suggested that the *.local_tasks.yml files that were used to define groups of tabs for the same "page" had been renamed to *.links.task.yml instead.

When I renamed optimizely.local_tasks.yml to optimizely.links.task.yml, the tabs were rendered again.

The source article below later confirmed this hunch.

Previous post on the format and contents of this file is at http://optimizely-to-drupal-8.blogspot.com/2014/05/implementing-multiple-tabs-on-page.html


Source:

YAML files for menu links, contextual links, local tasks, and local actions have been renamed
https://www.drupal.org/node/2302893

Aug 27, 2014

Defining a menu item in the system hierarchy

In D7 adding a normal menu item to the system hierarchy, for example, at the end of

  Home >> Administration >> Configuration >> System

involves defining the menu item in hook_menu() using something like this:

  $items['admin/config/system/optimizely'] = array(
    'title' => 'Optimizely',
    'description' => 'List of all ...',
    'type' => MENU_NORMAL_ITEM,

    // Other properties for  'page callback', 'file', etc.
  );

In Drupal 8 this is done quite differently. For the Optimizely module, there is a new file placed at the module root,

  optimizely.links.menu.yml

whose contents define menu items such as,

  optimizely.listing:
    title: Optimizely
    description: 'List of all ...'
    route_name: optimizely.listing
    parent: system.admin_config_system


route_name refers to a route that is defined in the optimizely.routing.yml file.

  optimizely.listing:
    path: /admin/config/system/optimizely
    defaults:
      _form: \Drupal\optimizely\ProjectListForm
      _title: Optimizely
    requirements:
      _permission: administer optimizely

To find the name of the menu item for the parent property, I did a string search of the path 'admin/config/system' across the core routing files. That turned up the route system.admin_config_system in the file system.routing.yml. Looking in system.links.menu.yml confirmed that system.admin_config_system is also the name of the corresponding menu item.

The name of the menu item, optimizely.listing, is by convention usually identical to its route_name. That's not a requirement, but it is a common convention that readily associates the menu item together with its route.

In D7 hook_menu() serves multiple purposes, whereas Drupal 8 distinguishes between routes and menu items and defines them in different files.

Sources:

Menu and routing system
https://api.drupal.org/api/drupal/core!includes!menu.inc/group/menu/8

Aug 22, 2014

Drupal 8: alpha 13 --> alpha 14

The story of Drupal 8 continues as it moves through alpha releases. Here are a number of changes I had to make for our module when I upgraded from alpha 13 to alpha 14.

  - - - - -
Function module_exists() has been removed. 

Instead, use \Drupal::moduleHandler()->moduleExists().

The second source article below lists a bunch of Module/hook system functions that are being removed. This leads me to believe that there may be a lot of other core hooks that are currently deprecated which will also disappear by the time the first core beta rolls out.

  - - - - -
The signatures for buildForm(), validateForm(), and submitForm() in class FormBase have changed.

Old method signatures.

  function buildForm(array $form, array &$form_state)
  function validateForm(array &$form, array &$form_state)
  function submitForm(array &$form, array &$form_state)

New signatures.

  use Drupal\Core\Form\FormStateInterface;

  function buildForm(array $form, FormStateInterface $form_state)
  function validateForm(array &$form, FormStateInterface $form_state)
  function submitForm(array &$form, FormStateInterface $form_state)


Interestingly enough, you can index into the $form_state parameter as though it is still an array even though its type is now that of an interface. For example, the following code remains unchanged.

    $oid = $form_state['values']['optimizely_oid'];

It turns out that in PHP you can iterate through the visible properties of an object using statements such as foreach.

Update as of alpha 15: you can no longer treat the $form_state param like an array. See the post  http://optimizely-to-drupal-8.blogspot.com/2014/09/formstateinterface-and-drupalvalidpath.html

  - - - - -
Method ConfirmFormBase::getCancelRoute() has been renamed to getCancelUrl()The method returns an object of class Url as previously documented.

  - - - - -
Method FormBuilder::setErrorByName() has been moved to FormStateInterface::setErrorByName() and has a different signature.

Old signature:

  function setErrorByName($name, array &$form_state, $message = '')

New signature:

  function setErrorByName($name, $message = '');

This function can be called in some methods by using the $form_state param, which is now typed as FormStateInterface. For example,

  function validateForm(array &$form, 
                        FormStateInterface $form_state) { 

    $form_state->setErrorByName('optimizely_project_code',
        $this->t('The Optimizely Account ID must be set.'));

}

  - - - - -
To redirect from a form, use FormStateInterface::setRedirect().

Previously, one way to indicate redirection after a form was submitted and processed was, for example:

  $form_state['redirect_route']['route_name'] = 'optimizely.listing';

Now redirection is provided by:

  $form_state->setRedirect('optimizely.listing');

where $form_state is typed as a FormStateInterface. 

  - - - - -
Method TestBase::randomName() has been renamed back to randomMachineName().

  - - - - -
The PHP fileinfo extension is required. The Drupal 8 installer will check for this and issue an error if the extension is not enabled.

  - - - - -

Sources:

Replace module_exists with \Drupal::moduleHandler()->moduleExists
https://www.drupal.org/node/2116375

Module/hook system functions replaced with module_handler service
https://www.drupal.org/node/1894902

Object Iteration
http://php.net/manual/en/language.oop5.iterations.php

Aug 16, 2014

Producing a new release for a Drupal.org contrib module

I recently produced my first release of the Optimizely module for Drupal.org. There are several steps involved, which I am documenting here in a brief overview of the process specifically for Drupal.org.

The sources listed below give a lot more of the details.

I am assuming that you're familiar with the basic concepts and commands for using git to clone a repo, make commits, manage branches, etc.

(0)  Create a release branch within your development repo that will contain your work. You may be using an existing branch, in which case you don't need to do this.

The required convention for naming branches for contrib modules looks like these examples.

  7.x-3.x
  8.x-2.x

The first part, before the hyphen, refers to the major version of Drupal core. The second part refers to the major version of the module you're working on.

Exact version numbers for specific releases are provided in a separate step by doing a git tag.

N.B. On Drupal.org, we are told not to use the master branch.

(1)  When the code in your dev repo is ready for release, git push the branch to the repo on Drupal.org.

(2)  In your dev repo, git tag with a tag such as the following.

  7.x-3.5-alpha3
  8.x-2.14-beta1
  7.x-1.0

The recognized suffixes are:  unstable  alpha  beta  rc
These must be lowercase, and there must be a number appended to the suffix if you use one.

  $  git  tag  8.x-2.14-beta1

After tagging the code in the dev branch, git push the tag to Drupal.org.

  $  git  push  origin  8.x-2.14-beta1

(3)  On the module's page on Drupal.org, go to Edit > Releases. Click on Add new release to create a new release.

(4)  Finally, on Edit > Releases, check Show snapshot release if you want the release to appear on the module's front page in the list of releases. You may not need to do this if it's been done before for the branch you're working on.

It may take a minute or two for the new release to show up on the site.


Sources:

On the Version control tab of a module's page on Drupal.org there are excellent instructions that include actual commands to use. For example, 
https://www.drupal.org/node/1305958/git-instructions/8.x-2.x

Creating and testing full projects and releases
https://www.drupal.org/node/1015224

Release naming conventions
https://www.drupal.org/node/1015226

Creating a project release
https://www.drupal.org/node/1068944


Aug 12, 2014

Be careful using TestBase::randomString() to generate test string values

While converting a test class to D8, I simply replaced some calls to TestBase::randomName() with calls to TestBase::randomString(), thinking that the generated strings would then be more varied and provide slightly better test coverage.

This "innocuous" change resulted in test assertions that would sometimes fail, sometimes pass.  Repeated test runs showed no particular pattern while I was debugging, not suspecting this particular change I had made but looking elsewhere instead.

With hindsight, I realize that the failing tests did not happen consistently because these are randomized string values. Of course!

The broken code:

    $edit = array(
      'new_project_title' => $this->randomString(8),

      // ... values for other form fields ...
    );
    $this->drupalPostForm($this->addUpdatePage, $edit, t('Add'));

    $project_title = db_query(
      'SELECT project_title FROM {optimizely}' . 

      ' WHERE project_title = :new_project_title',
       array(':new_project_title' => $edit['new_project_title']))
        ->fetchField();



The original code, which works:

    $edit = array(
      'new_project_title' => $this->randomName(8),

      // ... values for other form fields ...
    );
    $this->drupalPostForm($this->addUpdatePage, $edit, t('Add'));

    $project_title = db_query(
      'SELECT project_title FROM {optimizely}' . 

      ' WHERE project_title = :new_project_title',
       array(':new_project_title' => $edit['new_project_title']))
        ->fetchField();



randomName() returns a string consisting only of letters and numbers.
randomString() returns a string consisting of any printable character.

The problem is that randomString() sometimes generates a string with special characters that results in the forming of invalid SQL statements. I had made no provision for properly escaping those.

Sources:

public function TestBase::randomString
https://api.drupal.org/api/drupal/core!modules!simpletest!src!TestBase.php/function/TestBase%3A%3ArandomString/8

Aug 9, 2014

Renamed methods in class WebTestBase and other mismatches


For the most part, the methods of class WebTestBase in Drupal 8 are identical in name and functionality to the methods of class DrupalWebTestCase in D7.

However, there are some exceptions.

(1)  Both classes have a method named drupalPost(), but they do not have the same signature. It turns out that the counterpart of DrupalWebTestCase::drupalPost() is WebTestBase::drupalPostForm().

(2)  Similarly, the counterpart of DrupalWebTestCase::drupalPostAJAX() is WebTestBase::drupalPostAjaxForm().

From a Drupal coding standard: "If an acronym is used in a class or method name, make it CamelCase".

(3)  The return value of WebTestBase::drupalCreateNode() is of type NodeInterface. That's very different from what is returned by DrupalWebTestCase::drupalCreateNode(), which is an object converted from an array via a (object) typecast.

For example, to access the node id of the created node in D7, you use

    $node1 = $this->drupalCreateNode($settings);
    $id = $node1->nid;


But in D8,

    $node1 = $this->drupalCreateNode($settings);
    $id = $node1->id();



Update: see  http://optimizely-to-drupal-8.blogspot.com/2014/12/beta-3-beta-4-configuration-schema-and.html

Sources:

abstract class WebTestBase
https://api.drupal.org/api/drupal/core!modules!simpletest!src!WebTestBase.php/class/WebTestBase/8

interface NodeInterface
https://api.drupal.org/api/drupal/core!modules!node!src!NodeInterface.php/interface/NodeInterface/8

drupalPost() and drupalPostAJAX() have been renamed
https://www.drupal.org/node/2087433 

Object-oriented code
https://www.drupal.org/node/608152#naming

Aug 6, 2014

Drupal 8: alpha11 --> alpha13: AliasManager, hook_help(), getCancelRoute()

In upgrading the Optimizely module from Drupal 8, alpha 11 to alpha 13, I had to make the following changes. Yep, the D8 APIs have not been frozen!

  -----
The class Drupal\Core\Path\AliasManager has had two of its methods renamed.

getPathAlias() --> getAliasByPath()
getSystemPath() --> getPathByAlias()

For the earlier post about using the Alias Manager, see  http://optimizely-to-drupal-8.blogspot.com/2014/06/function-drupallookuppath-has-been.html

  -----
The signature for hook_help() has changed.

alpha 11:  hook_help($path, $arg)

alpha 13:  hook_help($route_name, $route_match)

In other words, the first parameter is no longer a url path. Instead, it is the name of a route as defined in the module's .routing.yml file or possibly as defined elsewhere.

Example:
function optimizely_help($route_name, $route_match) {
  switch ($route_name) {

    case 'help.page.optimizely':
      return t('Optimizely is a third party service ...');

    case 'optimizely.listing':
      return t('... A listing of the Optimizely ...');
    // Other cases ...
  }
}

Note the special route in the first case of the switch statement. It corresponds to path admin/help#optimizely for the site's general help pages.

The obsolete post on hook_help() is at  http://optimizely-to-drupal-8.blogspot.com/2014/05/hookhelp-is-same-as-in-d7.html hook_help() is no longer unchanged from Drupal 7.

  -----
Fatal error: Call to a member function toRenderArray() on a non-object in  ...\opti\core\lib\Drupal\Core\Form\ConfirmFormHelper.php on line 44

This error message came up when I clicked on a link to bring up a delete confirmation form.

After looking at the code in class ConfirmFormHelper and checking how it is used by other core classes, I changed the return value of getCancelRoute() in my own class DeleteForm like this.

Before. 

  public function getCancelRoute() {
    return array('route_name' => 'optimizely.listing');
  }


After. 

use Drupal\Core\Url;

  public function getCancelRoute() {
    return new Url('optimizely.listing');

  }

For the earlier post about creating a delete confirmation form, see http://optimizely-to-drupal-8.blogspot.com/2014/07/building-delete-confirmation-form-with.html


Sources:

function hook_help()
https://api.drupal.org/api/drupal/core!modules!system!system.api.php/function/hook_help/8

Creating a content entity type in Drupal 8
http://jmolivas.com/creating-a-content-entity-type-in-drupal-8
"Call to a member function toRenderArray() on a non-object"

Releases for Drupal core
https://www.drupal.org/node/3060/release?api_version[]=7234

Aug 4, 2014

Autoloading at module install time from module itself == problem

In my last post I described implementing hook_page_build(), which along with other hooks is defined in the .module file.

This function needs to look up paths and aliases. In our module that functionality is provided by trait LookupPath which I described in  http://optimizely-to-drupal-8.blogspot.com/2014/07/drupal-8-requires-php-54-or-higher-and.html.

Because hook_page_build() is a global-level PHP function and therefore does not provide a class context, I was not able to directly use the trait.

I took the expedient approach of defining a very thin wrapper class within the .module file. It's not pretty, but I figured it would work.

  class LookupPathWrapper {
    use Drupal\optimizely\LookupPath;
  }

A use of this wrapper would be, for example,

  $path_alias = LookupPathWrapper::lookupPathAlias($proj_path);

In fact, it did work for quite a while. During development I would edit the .module file, clear caches, and continue.

Then I uninstalled the module and re-installed. That's when things went seriously awry.

I kept getting an error about not being able to find the trait in the .module file. Every page of the site was broken. Even after emptying all the cache tables in the database and deleting all the subdirectories under sites/default/files, the site was completely broken.

The module seemed to be partly installed, partly not. Its database table had not been created, the absence of which triggered other error messages as I muddled through this mess.

I ended up re-installing Drupal repeatedly as I tried to figure out how to use the trait. The critical factor was that the trait is part of the module that is being installed.

Finally, I just wrote two ordinary functions that repeat the two lines of code in the trait's methods and placed them directly into the .module file, like the following. The module now installs fine.

function _lookup_path_alias($path) {

  $alias = \Drupal::service('path.alias_manager')

             ->getPathAlias($path);
  return (strcmp($alias, $path) == 0) ? FALSE : $alias;
}


function _lookup_system_path($alias) {

  $path = \Drupal::service('path.alias_manager')

            ->getSystemPath($alias);
  return (strcmp($path, $alias) == 0) ? FALSE : $path;
}


A different approach would be to place the functions into a file to require for re-use in different places, but I didn't want to bother with the refactoring that would have entailed.

In short: it looks like autoloading of the classes and traits of a module does not work when you are in the process of installing the module itself.

This article for Drupal 7 sounds closely related:

autoloading won't work during module install
https://www.drupal.org/node/2078587

Also,  

psr0
https://www.drupal.org/project/psr0

Aug 2, 2014

hook_init() is gone, R.I.P. and drupal_add_js() deprecated

In Drupal 8 hook_init() has been removed. Generally speaking, it's described as being replaced by the Symfony components that provide access to events and to listening for them.

In our case, however, we have the relatively simple need of adding to certain pages the externally provided javascript code snippets that drive the use of the Optimizely service.

Given this need, I was able to use the existing hook_page_build() function instead of hook_init().

Drupal 7's drupal_add_js() has been removed. Actually, it has been renamed to _drupal_add_js() but it is strongly deprecated in favor of using the #attach key in render arrays.


Here's the relevant D7 code.

function optimizely_init() {
 
  $current_path = current_path();

  // Determine whether to process the current page, etc.
  // Other processing, setup, etc.

  $snippet_url = 'http://cdn.optimizely.com/js/' .
                  $project['project_code'] . '.js';

  drupal_add_js($snippet_url, 'external');
}



And the corresponding D8 code.

function optimizely_page_build(&$page) {

  $current_path = current_path();

  // Determine whether to process the current page, etc.
  // Other processing, etc.

  $snippet_url = 'http://cdn.optimizely.com/js/' .
                  $project['project_code'] . '.js';
                 
  $page['#attached']['js'][] = array(
                                  'type' => 'external',
                                  'data' => $snippet_url,
                                );

} 


Update: see  http://optimizely-to-drupal-8.blogspot.com/2015/01/beta-4-hookpagebuild-has-been-removed.html  for several changes applicable to this code.


Sources:

hook_init() removed
https://drupal.org/node/2013014

function hook_page_build
https://api.drupal.org/api/drupal/core!modules!system!system.api.php/function/hook_page_build/8

function _drupal_add_js
https://api.drupal.org/api/drupal/core!includes!common.inc/function/_drupal_add_js/8