Dec 27, 2014

Beta 3 --> Beta 4: Configuration schema and metadata


When I attempted to run the automated test suite for the module, several of the tests failed that had passed in the previous beta of D8.

But the following fatal error was also displayed:

Uncaught PHP Exception Drupal\Core\Config\Schema\SchemaIncompleteException: "No schema for optimizely.settings" at /var/www/html/opti/core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php line 91

For the module, optimizely.settings is the name of a group of configuration settings that are implemented using the Simple Configuration API, which I blogged about in this post.

So what happened?

As of Beta 4, by default the TestBase class and its derived classes such as WebTestBase are required to provide schemas for configuration data. This can be done through use of a .schema.yml file.

For example, for the optimizely module, I created the file

  config/schema/optimizely.schema.yml

whose contents are

  optimizely.settings:
    type: mapping
    label: 'Optimizely Config Data'
    mapping:
      optimizely_id:
        type: integer
        label: 'Optimizely ID Number'
        translatable: false


The type property indicates that this schema is to provide static mappings. The mapping property then describes one or more settings. In this case, there is just the one setting optimizely_id.

Besides integer, other commonly used types would be string and text. There are several others that are pre-defined, and you can also define your own type.

The translatable property is false by default but I decided to explicitly code it to emphasize that a primary purpose of these schemas is to document items that may need to be translated.

For each group of settings such as optimizely.settings, there may be an additional file named optimizely.settings.yml that provides other data about those settings, such as their values. I didn't need such a file here, but the source articles give examples and a lot of info.

One final thing: if you really don't want to adhere to this requirement, you can disable the check for the presence of schemas by including the following line in your test class. However, this is an expediency that is discouraged.

  protected $strictConfigSchema = FALSE;


Update: the langcode key is now required for automated testing. See http://optimizely-to-drupal-8.blogspot.com/2015/12/drupal-8-beta-14-beta-15-missing.html

Sources:

All TestBase derived tests now enforce strict configuration schema adherence by default
https://www.drupal.org/node/2391795

Configuration schema/metadata
https://www.drupal.org/node/1905070

Dec 24, 2014

Beta 3 --> Beta 4: Use libraries, not individual stylesheets or javascript files

I installed Drupal 8 beta 4 with hopes that I would not have to make any code changes other than bumping up the version number. Alas, it was not to be.

As soon as I tried to go to the module's configuration page, I got the unhelpful error message, 

  The website has encountered an error. Please try again later.

Going to admin/reports/dblog showed the log message:
  
LogicException: You are not allowed to use css in #attached in drupal_process_attached() (line 1759 of /var/www/html/opti/core/includes/common.inc).

Searching for "css" through the change records for Drupal core revealed that css and javascript files are now organized and declared within libraries that are defined in the .libraries.yml file.

For example, in the D7 version there are lines of code such as the following when building a form,

  $form['#attached']['css'] = array(
    drupal_get_path('module', 'optimizely') . 

      '/css/optimizely.theme.css',
  );
  $form['#attached']['js'] = array(
    drupal_get_path('module', 'optimizely') . 
      '/js/optimizely.admin.js',
  );
 

In D8, we have instead yet another YAML file optimizely.libraries.yml whose contents are,

  optimizely.forms:
    version: VERSION
    css:
      theme:
        css/optimizely.theme.css: {}

  optimizely.enable:
    version: VERSION
    js:
      js/optimizely.admin.js: {}
    dependencies:
      - core/jquery
      - core/drupalSettings


In this example, two libraries optimizely.forms and optimizely.enable are defined. Both css and javascript can be part of the same library, but I separated them for finer granularity.

The relative paths to the files are relative to the root of the defining module.

For the css property, the theme sub-property or some other sub-property is required for a valid definition. Otherwise, the attempted inclusion of the css will silently fail. See the article CSS file organization (for Drupal 8).

The dependencies property is critical. After a couple of hours of debugging, I realized that the JavaScript was failing because jQuery is no longer automatically included in every page, so it must be explicitly specified. The other dependency is a variable that is referenced that is part of drupalSettings.

VERSION seems to be a filler value that means no particular version.

To use these libraries,  the equivalent form building code then has the lines,

  $form['#attached']['library'][] = 
    'optimizely/optimizely.forms';

  $form['#attached']['library'][] = 
    'optimizely/optimizely.enable';
 


Sources:

Adding stylesheets (CSS) and JavaScript (JS) to a Drupal 8 module
https://www.drupal.org/node/2274843

hook_library_info() is replaced by *.libraries.yml file
https://www.drupal.org/node/2201089

CSS file organization (for Drupal 8)
https://www.drupal.org/node/1887922

Change records for Drupal core
https://www.drupal.org/list-changes

Dec 22, 2014

Beta 3 --> Beta 4: "Routes use _controller instead of _content"

When defining routes in the .routing.yml file, the _content property is no longer valid. I got a "page not found" error when browsing to such a route.

Use _controller instead.

Old:

optimizely.add_update.oid:
  path: /admin/config/system/optimizely/add_update/{oid}
  defaults:
    _content: \Drupal\optimizely\DoUpdate::buildUpdateForm
    _title: Optimizely Edit Project
  requirements:
    _permission: administer optimizely


New:

optimizely.add_update.oid:
  path: /admin/config/system/optimizely/add_update/{oid}
  defaults:
    _controller: \Drupal\optimizely\DoUpdate::buildUpdateForm
    _title: Optimizely Edit Project
  requirements:
    _permission: administer optimizely



Source:

Routes use _controller instead of _content
https://www.drupal.org/node/2378809

Dec 16, 2014

Migrating the Optimizely module to Linux Mint

Migrating the Optimizely module to Linux Mint for local development involved three steps:

- installing Drupal 8 beta 3
- installing the module and testing it manually
- running the module's automated test suite

This all went pretty smoothly with minimal work, which is very encouraging as far as transitioning to doing Drupal development in a Linux system.

The one important point I'd like to make in this post is that to chown the entire Drupal instance to www-data:www-data as the owner:group seems to take care of all the owner and permission problems I've had in the past.

The rest of this posting is pretty routine stuff but potentially useful for anyone doing a similar migration.


(1) Installed Drupal 8 beta 3.

Edited /etc/apache2/apache2.conf to insert the following directive, where opti is the site root under the web server root.

<Directory /var/www/html/opti>
    AllowOverride All
</Directory>


Changed the owner of the entire site instance to the user that Apache apparently runs under.

# cd /var/www/html
# chown -R www-data:www-data opti



(2) Installed the Optimizely module.

I copied over the module tree from my old development system. I could have just cloned the repo from Github instead.

# cd /var/www/html/opti/modules/
# mkdir contrib

# cp -r ....../optimizely contrib

And changed the owner of contrib and its subdirectories.

# chown -R www-data:www-data  contrib

These steps were sufficient to get the module installed, enabled, and functioning.


(3)  Ran the module's automated test suite.

When I tried to enable the Testing core module, I got the error message

The testing framework could not be installed because the PHP cURL library is not available.

So,

# apt-get install php5-curl

This then allowed the Testing module to be enabled.

Successfully ran the entire suite of nine tests. No fails, no exceptions.

The test run finished in 8 min 42 sec.

Interestingly, I did not have to increase the php max execution time as was necessary when I ran this suite in a Linux Mint virtual box on a old Windows 7 host.

Dec 13, 2014

Running out of disk space on Linux Mint partition, changing the MySQL data directory

I bought a PC with Linux Mint 17 already installed. This is my first time working on a Linux system, so there's been a bit of a learning curve.

Things were going along pretty well until I started to get all kinds of weird errors.

• A text file that I maintain manually had the last part of its contents truncated from the last time I had saved it.

• I could not log in to phpMyAdmin even though I knew the username and password were correct.

• An initial install of Drupal 7 would not come up with its front page. A PDO error on a SQL query was being reported.

Eventually I suspected something related to databases, happened to run the Disk Usage Analyzer and found that one of the disk partitions had run out of space. Oh, joy.

It turns out that the system I got was configured with three partitions. On the File Systems tab of the System Monitor app, it shows the partitions and their total sizes as

/dev/sda1   /boot  463.9 MiB
/dev/sda5   /       18.6 GiB
/dev/sda6   /home  215.4 GiB


By default, MySQL 5.5 stores its data on /var/lib/mysql which is on the / partition which has about 18 GiB allocated to it.

That partition was not large enough for the Drupal 7 database along with everything else that was there, which apparently includes the system itself and apps that I had earlier installed.

Looking at these space allocations, I inferred that /home is where user data of all sorts is intended to be kept.

So I used the sources listed below to figure out how to change where MySQL stores its data.

(0) But first, I figured I should clean up the mess I had already created by uninstalling phpMyAdmin, and then completely uninstalling MySQL.

(I carried out all the commands in this post while logged in as root. Alternatively, you could use sudo instead.)

# apt-get remove phpmyadmin

# /etc/init.d/mysql  stop
# apt-get remove mysql-server
# cd /var/lib
# rm -r mysql

# apt-get autoremove

That last command is to remove any packages that are no longer needed on the system. I'm pretty fussy about not keeping useless clutter around.

(1) Then, re-installed MySQL 5.5.

# apt-get install mysql-server

(2) Changed the MySQL data directory to use the much larger partition. In my case, I chose to put the data directly under /home

# /etc/init.d/mysql  stop
# cp -R -p  /var/lib/mysql  /home

# cd /var/lib
# rm -r mysql


# ln -s  /home/mysql  /var/lib/mysql


That last command is to provide a symbolic link from the default directory to the real one. Otherwise, you can edit the datadir setting in the MySQL configuration file /etc/mysql/my.conf

Note that the MySQL logs are still located in  /var/log/mysql

(3) The final step was to deal with the AppArmor app.

Edited /etc/apparmor.d/usr.sbin.mysqld to change from

  /var/lib/mysql/ r,
  /var/lib/mysql/** rwk,


to

  /home/mysql/ r,
  /home/mysql/** rwk,


I couldn't find the apparmor app mentioned in the source articles. Instead, I rebooted in order for these AppArmor security changes to take effect. (Could the AppArmor functionality be baked into the Linux Mint system?)


Sources:

How to change the MySQL data default directory
http://www.ubuntugeek.com/how-to-change-the-mysql-data-default-directory.html

Linux Ubuntu move mysql database to other path in 5 minutes
http://article.my-addr.com/?show=linux_ubuntu_change_datadir-move_mysql_database_to_other_path

How to Remove MySQL Completely from Linux System
http://tecadmin.net/remove-mysql-completely-from-linux-system/

Dec 3, 2014

Setting up xDebug with Sublime Text under Linux Mint

I wrote an earlier post about setting up the xDebug PHP debugger with the Sublime editor under Windows 7.

I'm now migrating to developing on a Linux system and have done this set up for Sublime Text 3 and Linux Mint 17.

This post borrows heavily from the earlier article. 

(1) Installed xDebug 2.2.3 extension for PHP.

(a) While logged in as root,

  # apt-get  install  php5-xdebug

This installed the php5-xdebug package, creating the two files

  /usr/lib/php5/20121212/xdebug.so
  /etc/php5/apache2/conf.d/20-xdebug.ini

On your system, the path for xdebug.so may be a little different.

20-xdebug.ini is actually a link to the file mentioned in step (2) below.

(b)  Restarted Apache. Loaded a page containing a call to phpinfo() to check that the extension was installed. On the page there appeared an xdebug section with several tables that showed settings and directives.

(2) Added directives for xDebug.

Edited /etc/php5/mods-available/xdebug.ini to include the following lines.

  xdebug.remote_enable = On
  xdebug.remote_host = "localhost"
  xdebug.remote_port = 9000
  xdebug.remote_handler = "dbgp"
  xdebug.remote_autostart = On
  xdebug.remote_mode = req
  xdebug.remote_connect_back = 0 


Because I only want to debug locally, I set xdebug.remote_connect_back to 0 to turn it off. I believe this should be set to 1 if you intend to debug a site running on a remote system.

(3) Installed the Sublime package manager, "Package Control".

(a)  Within Sublime, clicked Preferences > Browse Packages, then browsed up one level to find the full path for the Installed Packages folder. On my system the path is

  ~/.config/sublime-text-3/Installed Packages

(b)  Downloaded the file Package Control.sublime-package from 

   https://sublime.wbond.net/Package%20Control.sublime-package

(c)   Copied that file into the Installed Packages directory.

(d)  Re-started Sublime.

In Sublime's Command Palette window, various Package Control commands are then available on the palette and elsewhere.

(4) Using Sublime's Package Control, installed the Xdebug Client package.

  Tools > Command Palette ...  >  Package Control: Install Package

Typed Xdebug Client into the search field, and clicked on Xdebug Client in the search results to install.

This adds a submenu, Tools > Xdebug, or you can use the keyboard equivalents.

(5) To start an xDebug session.

To start a debugging session, I load the site by appending the query string ?XDEBUG_SESSION_START=1  to the initial url, like so.

  http://localhost/power-poetry/?XDEBUG_SESSION_START=1 

The value of  XDEBUG_SESSION_START  is a session name that is stored in a cookie, so it probably could be any name you choose.

There are other ways to start an xDebug session, but for getting started with using this debugger, this seems like the simplest.

Finally, in Sublime itself, to activate the editor as an xDebug client,

  Tools > Xdebug > Start  Debugging


Sources:

Debug PHP with Xdebug in Sublime Text 2 (in Linux mint)
http://saml.rilspace.org/debug-php-with-xdebug-in-sublime-text-2-in-linux-mint

XDEBUG EXTENSION FOR PHP
http://xdebug.org/docs/remote

martomo / SublimeTextXdebug
https://github.com/martomo/SublimeTextXdebug

Package Control
https://sublime.wbond.net/installation#st2

Download:  Package Control.sublime-package
https://sublime.wbond.net/Package%20Control.sublime-package

Nov 28, 2014

Beta 1 --> Beta 3: No module code changes !!

Running the Optimizely module under Drupal 8, beta 3, both manual and automated testing showed no loss of functionality or run-time errors. This is the first time that migrating to a new release of D8 did not require any code changes to the module.

All I had to do was bump up its version number. The reason I made a new release of the module is to show that it has been tested and conforms to the new D8 beta. My current convention is to incorporate the D8 version as part of the module version, like so:  8.x-2.15-beta3.


Nov 19, 2014

All blog posts have been updated for Beta 1

I have completed a review of all posts in this blog with respect to Drupal 8, beta 1, to check whether they are up to date.

Where there have been significant changes, rather than edit the original content, at the bottom of the article I added brief notes and links to newer posts that describe updates that have taken effect.

In one or two cases, there were enough code changes to warrant a fresh sample of code in a new posting.

The result is that this collection of articles remains reasonably up-to-date and accurate as we move into the beta releases of D8.

Happy Drupaling.

Nov 12, 2014

Implementing a form, D8 beta 1

I wrote an earlier post about the rudiments of how to implement a simple form for Drupal 8. There have been several related API changes since then, but that post has a number of notes that are still accurate and useful.

The original post is at http://optimizely-to-drupal-8.blogspot.com/2014/05/implementing-forms.html   You really should read that post first, then come back to this one for the current code.

In this posting, I am only providing an update of the code sample for the class that defines the form. You may want to read the earlier post as well.

Again, this is  only an extract from the class definition. To keep it short and to the point, I've omitted parts of the bodies of the methods as well as boilerplate comments. 

Code changes from the original post are in boldface.


namespace Drupal\optimizely;
 
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
 

class AccountInfoForm extends FormBase {

  public function getFormID() {
    return 'optimizely_account_info';
  }
 

  public function buildForm(array $form, 
                            FormStateInterface $form_state) {    
    $form['optimizely_id'] = array(      
      '#type' => 'textfield',
      '#title' => t('Optimizely ID Number'),
      '#size' => 60,
      '#maxlength' => 256,
      '#required' => TRUE,
    );
    $form['actions'] = array('#type' => 'actions', );
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => 'Submit',
    );

    return $form;
  }
 

  public function validateForm(array &$form,
                               FormStateInterface $form_state) {
    $oid = $form_state->getValue('optimizely_id');
 

    if (!preg_match('/^\d+$/', $oid)) {
      $form_state->setErrorByName(

        'optimizely_id',
        t('Your Optimizely ID should be numeric.'));    }
  }
 

  public function submitForm(array &$form, 
                             FormStateInterface $form_state) {
    // Code to update the database ...

    //    . . . . .

    drupal_set_message(t('The default project ...'),
                       'status');    
    // Redirect back to projects listing.
    $form_state->setRedirect('optimizely.listing');
    return;
  }
}
 

Related Posts.

Drupal 8: alpha 13 --> alpha 14
http://optimizely-to-drupal-8.blogspot.com/2014/08/drupal-8-alpha-13-alpha-14.html

FormStateInterface, and drupal_valid_path()
http://optimizely-to-drupal-8.blogspot.com/2014/09/formstateinterface-and-drupalvalidpath.html

Nov 9, 2014

hook_permission() has disappeared

As of Beta 1 and possibly a bit earlier, hook_permission() is gone. In its place, there is yet another YAML file.

Drupal 7 optimizely.module file,

  function optimizely_permission() {
    return array(
      'administer optimizely' => array(
        'title' => t('Administer Optimizely module'),
        'description' =>
          t('Administer access to everything in module'),
        'restrict access' => TRUE,
      ),
    );
  }


Drupal 8 optimizely.permissions.yml file,

  administer optimizely:
    title: 'Administer Optimizely module'
    description: 'Administer access to everything in module'
    restrict access: true


Exactly the same pieces of information are provided in the new definition as in the old. However, there are no calls to the t() function for translation purposes. In the source article, there is a comment that "We can then make sure static permissions run strings through t()", so it sounds like the intent is for t() to be called automatically as part of the processing of the file.

Obsolete post on hook_permission() is at http://optimizely-to-drupal-8.blogspot.com/2014/05/hookpermission-is-same-as-in-d7.html

Sources:

Defining permissions via $module.permissions.yml
https://www.drupal.org/node/2311427

Nov 8, 2014

Beta 1 released, more changes

It's great news that Drupal 8 Beta 1 was released. It implies that the API's have reached a reasonably stable (but not immutable) state.

In moving from the final alpha 15 to beta 1, I stumbled across the following.


(1)  The url() function has been removed. In its place, you can use \Drupal::url() which takes a route name, not a path. For example, instead of

  url('/admin/config/system/optimizely/settings')

use this call,  where optimizely.settings is a route that is defined in the module's .routing.yml file.

  \Drupal::url('optimizely.settings')

Where the route takes parameters, pass a second argument to url() that is a keyed array which provides the names and values of those parameters. For example, for the following route,

  optimizely.delete.oid:
    path: /admin/config/system/optimizely/delete/{oid}
    # The rest of the route definition ...

you can make a call such as,

  \Drupal::url('optimizely.delete.oid', array('oid' => $oid))

Caveat: you cannot use this function in hook_install(). Doing so will cause the install to bail out and leave things in a weird state that will likely leave you befuddled and frustrated. I got the generic, unhelpful message: The website has encountered an error. Please try again later.

So in hook_install() I removed the links in a message that is displayed upon completion of the installation. Some plain explanatory text is provided  instead.


(2)  The l() function takes a Url object as its second argument instead of a path. It is now in the Drupal class. Instead of

  l(t('Cancel'), 'admin/config/system/optimizely')

use something like this,

  \Drupal::l(t('Cancel'), new Url('optimizely.settings'))

However, same caveat as above. You can't use this function in hook_install() or it will bomb out and leave the install in a partially completed state. Not good.


Sources:

Drupal 8.0.0 beta 1 released
https://www.drupal.org/drupal-8.0.0-beta1

Remove most remaining url() calls
https://www.drupal.org/node/2340251


Sep 30, 2014

FormStateInterface, and drupal_valid_path()

As we move into Drupal 8, alpha 15, a few more API changes have come along for the Optimizely module.

(1)  For objects whose class implements interface FormStateInterface, you can no longer index into the object as though it were an array as in D7.

Instead, there are a number of methods to get and set values. In our case, we only need to retrieve values from simple form text fields.

So, for example,

function validateForm(array &$form, FormStateInterface $form_state)
{
  $proj_code = $form_state['values']['optimizely_project_code'];


is replaced with,

function validateForm(array &$form, FormStateInterface $form_state)
{
  $proj_code = $form_state->getValue('optimizely_project_code');


This change broke the code in a lot of the methods for validating and for submitting forms.


(2)  Regarding drupal_valid_path(), the function has been removed. So,

  if (drupal_valid_path($path, TRUE) == FALSE) {

is replaced with,

  if (\Drupal::pathValidator()->isValid($path) == FALSE) {


Sources:

Replace instances of deprecated drupal_valid_path with PathValidator
https://www.drupal.org/node/2303361






Sep 7, 2014

url() function in Twig templates

As of Drupal 8 alpha 14, several relative links that had been hard-coded in Twig templates were no longer working.

<li>Add the account ID to the 
<a href="/admin/config/system/optimizely/settings">
Account Info</a> settings page</li>

The url constructed by the theming system did not include the root of my development site, which is opti. Instead, it would come out as

  localhost/admin/config/system/optimizely/settings

Dropping the leading slash character in the href also produced an incorrect result. Here, add_update is the last part of the path for the page that is rendered using the template.

  localhost/opti/admin/config/system/optimizely/add_update/admin/config/system/optimizely/settings

After some poking around, I found the Twig url() function, which does the trick.

<li>Add the account ID to the 
<a href="{{ url('optimizely.settings') }}">
Account Info</a> settings page</li>

This produces the desired url, which is

  localhost/opti/admin/config/system/optimizely/settings

But note that the parameter to url() is the name of a route, not a path. In the optimizely.routing.yml file, the route is defined as

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


The path() and the l() functions might also work, but I haven't tried them. See the source articles.

Sources:

Creating and Using Templates
http://symfony.com/doc/current/book/templating.html

Create best practices about use of the l() function and the url() function in templates
https://www.drupal.org/node/1812562

Sep 2, 2014

Twig inline_template render elements

Updating to D8 alpha 14 resulted in one of the module's pages showing a form field of url paths literally like this,

  <ul><li>admin/config/system</li></ul>

In other words, there was HTML markup that was being escaped too many times and being rendered as content. Somehow, this issue appeared because of changes in alpha 14.

Researching this, I found two things. First, the core maintainers have been grappling with the same problem in the core UI itself. That's a bit of consolation for a newcomer like me swimming in the ocean of Drupal.

Second, for render arrays there is a new recognized value for the #type property. That value is inline_template. The idea is that when you don't want to create a full-blown Twig template file, this #type enables you to define a snippet of template in the render array itself.

♦  Here's a simplistic example.

  $render_title = array(
      '#type' => 'inline_template',
      '#template' => '{{ title }}',
      '#context' => array('title' => 'My Project'),
     );

  // Insert this array into the full form.
  $form['#project_title'] = $render_title;

The #template property provides the template using Twig syntax. The double braces mean print the value of the enclosed Twig variable, in this case, title.

The #context property provides a mapping from Twig variables to their values.

This example doesn't do anything except print the value of a variable. Depending on what might be in the value, you could just implement that without the use of a template.

♦  Here's something more useful and interesting.

  $render_link = array(
    '#type' => 'inline_template',
    '#template' => '<a href="{{ url }}">{{ update }}</a>',
    '#context' => array(
        'update' => $this->t('Update'),
        'url' => url('/admin/config/system/optimizely/add_update/'),
    ),
  );

  $form['#admin_link'] = $render_link;

The template consists of markup for a link to be rendered, using a translated word and a string that is built dynamically.

♦  Lastly, Twig templates allow for looping so that you can iterate over an array.

  $paths = array( ... );

  $render_paths = array(
      '#type' => 'inline_template',
      '#template' => '<ul>' .
        '{% for p in paths %}' .
          '<li>{{ p }}</li>' .
        '{% endfor %}' .
        '</ul>',
      '#context' => array('paths' => $paths),
     );

  $form['#paths'] = $render_paths;

The Twig for construct is like foreach in PHP. This builds an HTML unordered list with list items that are determined at runtime.

♦  Getting back to the issue that started me down this road, the original code was building snippets of markup and inserting the strings directly into the form render array. What was intended as markup would be escaped and displayed as content.

  $form['#paths'] = '<ul><li>some path</li></ul>';

With use of an inline template, the markup is handled correctly.

  $form['#paths'] = array(
    '#type' => 'inline_template',
    '#template' => '<ul><li>some path</li></ul>',
  );

♦  Another way: calling the t() function on a string that contains markup also produces the correct result. For example,

  $project_code = 
    t('Set ID in <a href="@url">@acct_info</a> page to enable',
      array('@url' => url('optimizely/settings'),
            '@acct_info' => t('Account Info'),
        )
      );


  $form['#project_code'] = $project_code;

Sources:

New inline_template render element for HTML code in PHP
https://www.drupal.org/node/2311123

Double escaping on /admin/modules/uninstall
https://www.drupal.org/node/2305831

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