WordPress from Development to Production using Docker

This post will cover how to use Docker for a local WordPress development environment and how to deploy it to an Ubuntu Linux server running docker.

What you will need.

  • Docker
  • VPS with Docker (for prod deployment)
  • WordPress

The first step is to get your local development environment set up for your WordPress site. There are quite a few ways I have setup this environment in the past. For the last year or so, I’ve been using Wodby’s Docker-based WordPress Stack with its many options.

Using git, clone docker4wordpress into a site folder. For example mysite.

git clone https://github.com/wodby/docker4wordpress.git mysite

Update the .env file, for example:

PROJECT_NAME=mysite
PROJECT_BASE_URL=dev.docker.mysite.com

Update your systems hosts file, for example:

127.0.0.1    dev.docker.mysite.com

Delete docker-compose.override.yml as it’s used to deploy vanilla WordPress

Download WordPress and uncompress it into the site folder so you end up with a wordpress folder. For example mysite/wordpress.

Optionally, you could download and extract WordPress using the CLI, for example:

cd mysite
wget http://wordpress.org/latest.tar.gz
tar xfz latest.tar.gz

Docker Compose

Update the docker-compose.yml volume paths to mount the codebase to ./wordpress instead of the root of the site folder. This step is needed for the build configuration. For example, replace ./:/var/www/html with ./wordpress:/var/www/html.

...
    volumes:
      - ./wordpress:/var/www/html

Create a volume definition to persist the mysql data. At the bottom of the docker-compose.yml file, uncomment and update the volumes node. For example, replace #volumes: with the following:

volumes:
  mysql:

Update the mariadb service to use the mysql volume. For example, under the mariadb service, uncomment and update the volumes node. For example:


services:
  mariadb:

    ...

    volumes:
      - mysql:/var/lib/mysql

With Docker running, from the site directory run docker-compose up -d to start containers.

If you’re in Windows and get the following error:

ERROR: for traefik Cannot create container for service traefik: Mount denied: The source path \\\\var\\\\run\\\\docker.sock:/var/run/docker.sock is not a valid Windows path

This worked for me using Cygwin. Before running docker-compose up -d, run export COMPOSE_CONVERT_WINDOWS_PATHS=1. For PowerShell, use $Env:COMPOSE_CONVERT_WINDOWS_PATHS=1 More info: github.com/docker/for-win/issues/1829

Now you’re ready to load the site in a browser, http://dev.docker.mysite.com:8000.

The WordPress install page should appear. After selecting your language, on the following screen, if using the default settings in .env file, enter wordpress for Database Name, Username and Password. The Database Host value is the service name defined in the docker-compose.yml, e.g., mariadb.

docker4wordpress - WordPress Install - Database Configuration
WordPress Install – Database Configuration

Custom Theme

To demonstrate getting a custom theme into the build, let’s make a copy of twentyseventeen and customize it.

cp -r wordpress/wp-content/themes/twentyseventeen wordpress/wp-content/themes/mytheme/

We’re just gonna make a small change to the site title text to show what you can do to make the font size flexible for the default below desktop viewport width.

Edit mytheme/style.css. Under Layout, add font-size: 5vw below the existing font-size rules. For example.

style.css
.site-title {
  ...

  font-size: 24px;
  font-size: 1.5rem;
  font-size: 5vw;

Login to the site and activate mytheme to see the change to the site title font size when you adjust the width of the browser below 768 pixels wide.

Plugins

To demonstrate the inclusion of plugins in the docker build, install a plugin that will be included in the codebase. For example, I like the Yoast SEO plugin. Instead of installing it using the dashboard, download and extract it. Copy the wordpress-seo folder into the wordpress/wp-content/plugins folder. You can verify the installation by logging into the site dashboard and inspecting the plugins page.

Docker Image

The docker image we build for staging and production will be based off the official WordPress image and will only need our themes, plugins and any other changes from the development environment. I created some build scripts to accept some parameters and copy files from the development environment into respective staging and production build folders.

If you’re in Windows, use the docker-build.ps1 PowerShell script.

If you’re in OS X or Linux, use the docker-build.sh bash script.

Deployment

After running the build script, a saved docker image .tar file should exist in the prod or stage folders. One way to deploy the image is to copy the .tar file to the server using scp and load it.

For example, with user gilfoyle, use scp from the stage folder.

# upload image
cd docker
scp mysite.tar gilfoyle@0.0.0.0:/home/gilfoyle/

Load the Docker image on the server.

# load image
ssh gilfoyle@0.0.0.0
docker load -i mysite.tar

Coming Soon, part two of this post to go over the VPS docker-compose.yml, migrating data, etc.

SSL Certificate Authority for Docker and Traefik

This post documents how to get https working on your local Docker development environment using Traefik as a reverse proxy for multiple services.

Step 1 – Root SSL Certificate

Create a sub directory to store generated keys, certificates and related files in your home folder, for example .ssl.

Using OpenSSL, generate the private key file, rootCA.key.

For Windows, use Cygwin, Git Bash, PowerShell or other Unix-like CLI.

mkdir .ssl
cd .ssl

openssl genrsa -des3 -out rootCA.key 2048

This root certificate can be used to sign any number of certificates you may need to generate for individual domains.

Using the key, create a new root SSL certificate file named rootCA.cer. This certificate will be valid for 10 years (3650 days).

openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.cer

Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Random
Locality Name (eg, city) []:Random
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Random
Organizational Unit Name (eg, section) []:Random
Common Name (e.g. server FQDN or YOUR name) []:Local Certificate
Email Address []:example@domain.com
  • If you need a .pem file, simply copy and rename the generated certificate file. For example, cp rootCA.key rootCA.pem

Import The Root Certificate

The host system needs to have the root certificate imported and set as trusted so all individual certificates issued by it are also trusted.

Run Win + R and enter mmc to open the Microsoft Management Console.

Then select File, Add/Remove Snap-ins.

Select Certificates from the available snap-ins and press the Add button.

From the Certificates Snap-in dialog, select Computer account > Local Account, and press the Finish button to close the window.

Then press the OK button in the Add or Remove Snap-in window.

Select Certificates and right-click Trusted Root Certification Authorities > All Tasks > Import to open the Certificate Import Wizard dialog. Browse for the rootCA.cer digital certificate file .

Open Keychain Access and go to the Certificates category in your System keychain.

Import the rootCA.pem using File > Import Items.

Double click the imported certificate and change the When using this certificate: dropdown to Always Trust in the Trust section.

Step 2 – Domain SSL Certificate

Create a sub directory in the .ssl folder for the local.docker.whoami.com domain certificate.

cd .ssl
mkdir local.docker.whoami.com

These steps are done in the local.docker.whoami.com folder.

Create an OpenSSL configuration file named server.csr.cnf.

server.csr.cnf
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[dn]
C=US
ST=RandomState
L=RandomCity
O=RandomOrganization
OU=RandomOrganizationUnit
emailAddress=noreply@local.docker.whoami.com
CN = local.docker.whoami.com

Create a v3.ext file for the X509 v3 certificate.

v3.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = local.docker.whoami.com

Create a certificate key named server.key for the local.docker.whoami.com domain using the configuration settings stored in the server.csr.cnf.

openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key -config <( cat server.csr.cnf )

Certificate signing request is issued using the root SSL certificate to create a local.docker.whoami.com domain certificate. The output is a server.crt certificate file.

openssl x509 -req -in server.csr -CA ../rootCA.pem -CAkey ../rootCA.key -CAcreateserial -out server.crt -days 730 -sha256 -extfile v3.ext

Browser - Root Certificate Import

Import the root CA certificate into the web browser trusted root certificate store.

Chrome: Settings (chrome://settings/) > Privacy and security, Manage certificates > Import, follow prompts to Browse for .ssl/rootCA.cer. In The Import Wizard Dialog | Certificate Store, Choose Automatically select the certificate store based on the type of certificate.

Firefox: Options > Privacy & Security, Certificates | View Certificates, Authorities > Import. Browse for the .ssl/rootCA.cer and select all 3 Trust checkboxes.

Traefik

Create a traefik project folder with a certs sub directory.

Copy the domain certificate and key into the traefik/certs folder and rename them to match the respective domain. For example,

cp server.crt traefik/certs/local.docker.whoami.com.crt
cp server.key traefik/certs/local.docker.whoami.com.key

In the traefik project folder, create this traefik.toml file.

traefik.toml
defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]
      [[entryPoints.https.tls.certificates]]
      certFile = "/etc/traefik/certs/local.docker.whoami.com.crt"
      keyFile = "/etc/traefik/certs/local.docker.whoami.com.key"

In in the traefik project folder, create this docker-compose.yml file to configure Traefik services and networks.

docker-compose.yml
version: "3"

services:
  proxy:
    image: traefik
    restart: unless-stopped
    command: --docker --logLevel=DEBUG
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./certs:/etc/traefik/certs
      - ./traefik.toml:/etc/traefik/traefik.toml

networks:
  default:
    external:
      name: webgateway

Create this docker-compose.yml file in the whoami project folder.

docker-compose.yml
version: "3"

services:
  app:
    image: emilevauge/whoami
    networks:
      - web
    labels:
      - "traefik.backend=whoami"
      - "traefik.frontend.rule=Host:local.docker.whoami.com"

networks:
  web:
    external:
      name: webgateway

Hosts

Add 127.0.0.1 local.docker.whoami.com to your computers hosts file

Docker

Create a network named webgateway.

docker network create webgateway

Bring up the traefik container followed by the whoami container using docker-compose. For example,

cd ~/traefik
docker-compose up -d

cd ~/whoami
docker-compose up -d

Navigate to https://local.docker.whoami.com in a browser that has imported the root CA certificate into its trusted root store.

Webpack 3 Sass cssnano Autoprefixer Workflow II

This follow up to the basic Webpack 3 Sass preprocessor workflow I wrote about here includes how to handle processing of relative URLs in the Sass for things like fonts and images. Ken Wheeler’s popular Slick carousel contains these asset types in its Sass, so let’s use that for this exercise.

Getting Started

Prerequisite: Node.js with npm.

Navigate to the project root in your CLI, such as Terminal, Cygwin or PowerShell.

Enter npm init to interactively create a package.json file. Accepting the default options is okay for now. The metadata in this file can be updated later as needed.

npm init

A package.json file should now exist in the root of the project. This json file is used to manage the project dependencies, set configuration options for supported modules and run npm scripts.

Create an index.html file in the project root with an unordered list of placeholder images. Include the dist/style.css link in the document head and dist/app.js link before the closing body tag. Add a class attribute with the value slider to the unordered list element. This sets it apart for selecting with our script and style code: <ul class="slider">

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" media="all" href="dist/style.css">
</head>
<body>
    <ul class="slider">
        <li>
            <h3>Slide 1</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 2</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 3</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 4</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 5</h3>
            <img src="https://placehold.it/800x600" />
        </li>
    </ul>
    <script async src="dist/app.js"></script>
</body>
</html>
  • Using Emmet, which is built into VS Code, you can create the index.html content by entering an exclamation mark on the first line then select the tab key.

Slick

Install jQuery and Slick carousel using npm.

npm install --save jquery slick-carousel

In the src/js folder, create an index.js JavaScript file to import jQuery and Slick carousel modules.

index.js
import $ from 'jquery'
import 'slick-carousel'

$('ul.slider').slick({
    dots: true
});

Before we can build this JavaScript a Webpack configuration and npm script will need to be defined.

Webpack & Babel

Install Webpack

npm install --save-dev webpack

Create a webpack.config.js file in the project root to transpile our ES6 JavaScript into ES5 for the browser using babel.

webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
context: path.resolve(__dirname, './src'),
  entry: {
    app: './js/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: '[name].js'
  },
  module: {
    rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
    ]
  },
  devtool: '#eval-source-map'
}

Install Babel

npm install --save-dev babel-core babel-loader babel-preset-env

Create a .babelrc configuration file in the project root.

.babelrc
{
  "presets": ["env"]
}

Here is how the project is structured so far.

  • project
    • .babelrc
    • index.html
    • package.json
    • webpack.config.js
    • src
      • js
        • index.js

Build

Install cross-dev for a consistent NODE_ENV variable when running NPM scripts on Windows and OS X.

npm install --save-dev cross-env

Define dev and build commands for npm-run-script in package.json.

package.json
...

  ],
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --watch --progress --colors",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

At the bottom of the webpack configuration, add the respective dev and build npm scripts using the NODE_ENV settings.

webpack.config.js
...

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}
  • Note that the ellipsis … in the code snippet above is not a part of the actual code and is there only to denote code that is being skipped and not applicable to the example. Keep this in mind when you encounter an ellipsis in the remaining snippets. To view the entire file, examine the source code.

Using the npm-run-scripts alias npm run to execute the build.

npm run build
  • For development, use npm run dev to watch for changes and build incrementally when changes are saved.

You should now have a new dist folder in the root of the project where the Webpack bundle is output.

The next page covers:

  • installing Webpack loaders to process Sass
  • installing Extract Text Plugin to output a style.css file
  • installing Autoprefixer PostCSS plugin
  • creating and importing Sass files
  • re-building and verifying the application output

Slick Lazy Load Photo Grid Using Webpack 3

How to layout and lazy load images in a flexible grid similar to how facebook displays them in a post. Selected images open a lightbox for previewing within a carousel. Image alt text is converted into a caption below the image.

Features

  • Lazy image loading
  • ES6 transpiler
  • JavaScript source maps
  • Sass CSS preprocessor
  • PostCSS Autoprefixer
  • CSSnano
  • Webpack 3
uiCookbook Photogrid
Demo
Source Code

Getting Started

If you don’t already have Node.js installed, then that needs to be the first order of business. Head on over to Node.js and get that taken care of.

Navigate to the project root in your CLI, such as Terminal, Cygwin or PowerShell.

Enter npm init to interactively create a package.json file. Accepting the default options is okay for now. The metadata in this file can be updated later as needed.

npm init

A package.json file should now exist in the root of the project. This file will be used later with the npm command when installing modules and running scripts.

Create an index.html file in the root of the project. Add an unordered list of images to the body with the .photogrid style class. For lazy image loading, instead of using a src attribute, put the image path in a data attribute named data-src. Also include the dist/style.css link in the document head and dist/app.js link before the closing body tag.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" media="all" href="dist/style.css">
</head>
<body>
    <ul class="photogrid">
        <li>
            <img data-src="https://images.unsplash.com/reserve/unsplash_528b27288f41f_1.JPG?auto=format&fit=crop&w=2700&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Sea breeze and splashing waves. A photo by @dankapeter on Unsplash"/>
        </li>
        <li>
            <img data-src="https://images.unsplash.com/photo-1494633114655-819eb91fde40?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Above it All. A photo by @anthonyintraversato on Unsplash" />
        </li>
        <li>
            <img data-src="https://images.unsplash.com/photo-1511125357779-27038c647d9d?auto=format&fit=crop&w=2551&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Found this beauty while being lost in the streets of Cancún, Mexico. A photo by @odiin on Unsplash" />
        </li>
        <li>
            <img data-src="https://images.unsplash.com/photo-1483919283443-8db97e2bcd81?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Touring NYC. A photo by @freddymarschall on Unsplash" />
        </li>
        <li>
            <img data-src="https://images.unsplash.com/photo-1487357298028-b07e960d15a9?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Wind turbines, Greece. A photo by @jeisblack on Unsplash" />
        </li>
    </ul>
    <script async src="dist/app.js"></script>
</body>
</html>
  • Using Emmet, which is built into VS Code, you can create the index.html content by entering an exclamation mark on the first line then select the tab key.

Lazy Image Loading

This is accomplished using David Walsh’s Simple Image Lazy Load and Fade method.

Create a folder named src. In that folder, create both a js folder and a sass folder.

Within the js folder, create an index.js app entry point file and a lazyimage.js module file.

Within the sass folder, create a style.scss file that will be used as the entry point for Sass processing. Add three Sass partials, _base.scss, _photogrid.scss and _lazyimage.scss. The leading underscore in the filename denotes that the file is a Sass partial and therefore will only be processed if imported.

  • photogrid
    • index.html
    • package.json
    • src
      • js
        • index.js
        • lazyimage.jsc
      • sass
        • style.scss
        • _base.scss
        • _lazyimage.scss

In the lazyimage.js module, export this Lazyimage ES6 class.

lazyimage.js
export default class Lazyimage {
  constructor(options) {

    this.init();
  }

  init() {

    [].forEach.call(document.querySelectorAll('img[data-src]'), function(img) {
      img.setAttribute('src', img.getAttribute('data-src'));
      img.onload = function() {
        img.removeAttribute('data-src');
      };
    });
  }
}

Import the lazyimage module in the app entry point.

index.js
import Lazyimage from './lazyimage'

new Lazyimage();

Add this Sass to the base partial for the unordered list and image element default style.

_base.scss
ul {
  padding: 0;
}

img {
  border-style: none;
  height: auto;
  max-width: 100%;
}

Add this Sass to the photogrid partial to apply the photogrid styling with flexbox to the list of images.

_photogrid.scss
ul.photogrid {
  margin: 0.5vw 0.5vw 0.5vw -0.5vw;
  font-size: 0;
  flex-flow: row wrap;
  display: flex;

  li {
    flex: auto;
    width: 200px;
    margin: 0.5vw;
  }
}

Add this Sass to the lazyimage partial to fade the image in when the image has loaded and the data-src attribute has been removed.

_lazyimage.scss
img {
  opacity: 1;
  transition: opacity 0.3s;
}

img[data-src] {
  opacity: 0;
}

Import the three partials into the style Sass file.

style.scss
@import "base";
@import "photogrid";
@import "lazyimage";

The next page covers adding Webpack and the build configurations, installing dependencies, running the first build and verifying the application output.


Webpack 3 Sass cssnano Autoprefixer Workflow

Basic Webpack 3 Sass preprocessor workflow using cssnano for postprocessing minification and optimization. The Autoprefixer PostCSS plugin is also included in this configuration to automatically add vendor prefixes for only the browsers that need to be supported.

Before getting started, install Node.js and NPM. The default package manager for the Node.js JavaScript runtime environment, NPM is included with the Node.js installation.

Project

Navigate to the project root in your CLI, such as Terminal in OS X, PowersShell in Windows.

Enter npm init to interactively create a package manifest file (package.json). As devDependencies are installed using the --save-dev option, the package.json file will be updated so the node modules can be re-installed using npm install or with its shorthand alias npm i.

npm init

Webpack 3

Webpack is a module bundler that generates static assets representing modules and their dependencies from a generated dependency graph. This enables a modular approach to web development that can be extended by using loaders with tasks that are performed when bundling files together.

Webpack version 3 was released earlier this year and as of this writing, version 3.8.1 is the latest. Install the latest version using npm install.

npm install --save-dev webpack

Open the project folder in a code editor, such as VS Code. Inspect the package.json file, webpack with its version number should now be listed under the devDependencies node. For example:

package.json
{
  "name": "myproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^3.8.1"
  }
}

Webpack Configuration

Create a new JavaScript file in the project root named webpack.config.js. At the top of this file define these required modules as constants: path, webpack and extract-text-webpack-plugin.

webpack.config.js
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

We’ve already installed webpack, and path is a core node module. Let’s install the Extract Text Plugin that will be used to extract the css from the bundle into a separate file.

npm install --save-dev extract-text-webpack-plugin

Next, create the webpack configuration object to export to the CLI.

webpack.config.js
...

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    /* app: './js/index.js',*/
    css: './sass/main.scss',
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'bundle.js'
  },
  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        // TODO - Add Loaders
      })
    }]
  },
  plugins: [
    new ExtractTextPlugin('style.css'),
  ]
}
  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Install Loaders

Before we define the loaders in the webpack configuration, let’s install them.

SASS Loader compiles Sass to CSS and since it requires node-sass, install both sass-loader and node-sass.

npm install --save-dev sass-loader node-sass

PostCSS Loader processes CSS with PostCSS.

npm install --save-dev postcss-loader

CSS Loader resolves import at-rules and url functions in the CSS.

npm install --save-dev css-loader

Style Loader inlines <style></style> in the DOM.

npm install --save-dev style-loader
  • To handle processing of relative URLs in the Sass for things like fonts and images, see part II.

Define Loaders

In our configuration, we are extracting the CSS to its own file with the ExtractTextPlugin. Therefore we will define our loaders for processing Sass and CSS in the plugins use option. The fallback option is used when the CSS is not extracted.

webpack.config.js
...

  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          {
            loader: 'css-loader',
            options: {
              minimize: true || {/* CSSNano Options */}
            }
          },
          {
            loader: 'postcss-loader'
          },
          {
            loader: 'sass-loader'
          }
        ]
      })
    }]
  },
  • Note that the loaders are ordered from bottom to top or right to left. Loaders act like functions, that’s why it’s from right to left. For example, css-loader(postcss-loader(sass-loader(resource)))

The next page covers installing and configuring the Autoprefixer PostCSS plugin, running the Webpack build and creating demo Sass and html to test the output.


Drupal 8 Custom Block Module Dev

For those ready to move beyond a simple “Hello World” module, this post documents building a Drupal 8 module with a Giphy search form in a custom block. The form uses jQuery to request data from the Giphy API and display the results.

Drupal 8 module to demonstrate custom block creation with the following features:

  • configuration data that is passed into the modules js library
  • public API search using core jQuery functions
  • twig template for a front end Giphy search form
Source Code

Module Folder

To get started, create a folder for the module in either /modules/custom/ or /sites/all/modules/. The name of the module folder is typically the same as the name given to the module. In the case of this particular module, giphys is an appropriate name. For example:

# context is drupal project root

cd modules
mkdir custom
mkdir custom/giphys

Module Info File

An info.yml file is needed to store module metadata. Since the module machine name is giphys, the file will be named giphys.info.yml

# create the giphys.info.yml file

cd custom/giphys
touch giphys.info.yml

Here are the metadata contents of this file. Included is data to inform Drupal of the core compatibility, module dependencies and a description for the administration portal interface.

giphys.info.yml
name: Giphys
type: module
description: 'Giphys is a Giphy search block'
core: 8.x
package: Other
dependencies:
  - block

After saving giphys.info.yml, in Drupal, select the Extend menu, e.g., http://drupal.docker.localhost:8000/admin/modules. Giphys should be listed under Other:

Extend | Giphys

Module File

The giphys.module file is the entrypoint used to define help and theme hook functions that return respective data and paths. .module files should only contain functions that implement hooks.

# create the giphys.module file

touch giphys.module
giphys.module
<?php

/**
 * @file
 * Module file for giphys_module.
 */

use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Implements hook_help().
 *
 * @see https://www.drupal.org/documentation/help-text-standards
 *
 * @see hook_help()
 */
function giphys_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.giphys':
      // Help text for the admin section, using the module name in the path.
      return t("This is help text created in giphys implementation of hook_help().");
  }
}

function giphys_theme($existing, $type, $theme, $path) {
  return [
    'giphys' => [
      'variables' => [
        'url' => 'http://example.com',
        'secret' => NULL
      ],
    ],
  ];
}

The next page covers twig templating for the HTML, module specific CSS and JavaScript asset management.


WordPress Post from Front End using REST API and Vue.js Part II

The objective of this post is to demonstrate some Vue.js basics for working with the WordPress REST API. This post continues where the last post left off with the following additions.

  • List posts submitted by the current user
  • Select from the list to edit a post
  • Delete a post from the list
Source Code

Development

The original proof of concept details building the WordPress plugin, installing Vue.js, Webpack and configuring for development. Also included is information on creating the front end slug where the Vuejs app will run. The end product is a simple form to submit new posts from a custom WordPress page template. It is recommended that you read these aforementioned prerequisites:

List Posts

App.vue

Above the form, replace the hardcoded heading text with a placeholder to render heading data that will be adding to the data model.

Below the form, add this markup to display the messages for loading and errors along with the list element for the posts. Note the vue.js v-if attributes to control visibility of the elements depending on the data model values. This is called conditional rendering.

<h3>{{ heading }}</h3>
<form v-on:submit.prevent="onSubmit">

...

</form>

<div v-if="loading">
    Loading...
</div>

<div v-if="error">
    {{ error }}
</div>

<div v-if="posts">
    <ul>
        <li v-for="post in posts">
            <a href="#" @click='editPost(post)'>{{ post.title.rendered }}</a>
        </li>
    </ul>
</div>
  • Note that an ellipsis … in the code snippets are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

In the App.vue component script block, update the data model for the new elements.

...

data() {
  return {
    error: null,
    heading: 'Submit New Post',
    loading: null,
    post_id: null,
    post_title: null,
    post_content: null,
    posts: null
  }
},

...

Insert the getPosts () method to fetch all the posts for the current user and set the posts property of the data model. The endpoints for the WordPress REST API can be looked up at its base url.

...

methods: {
  getPosts () {
    this.error = this.posts = null
    this.loading = true
    var params = '?author='+ wp_api_vuejs_poc.current_user_id +'&status=any';

    $.ajax({
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts' + params,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
    }, this ))
    .fail( $.proxy( function( response ) {
      this.error = response;
    }, this ));
  },

...

}

Insert the mounted function to call the getPosts method when the app has rendered. If there are any posts to show when the page loads, they should be listed.

App.vue
...

methods: {
  mounted: function () {
    this.$nextTick(function () {
      this.getPosts ();
    })
  },

...

}

Development Build

In your CLI, build in development mode which enables watch on the files and incrementally recompiles as needed. This is faster than a prodcution build. Additionally, this unminified build is compatible with the Vue DevTools extension for Chrome for debugging and object inspection.

npm run dev

After the build is completed and watch is running, reload the page. Submit a new post to make sure it is working and gets added to the list.

Edit / Add Posts

In the markup that lists posts, the titles link contains a @click='editPost(post) attribute to call the editPost method passing it the post object. Insert the editPost method to accept the post object from the list and set the properties of the data model. Since this is a simple proof of concept, we are not comparing the post against the version on the server to make sure it is hasn’t been modified since the last fetch. Note that the heading property is being set as well to display the appropriate text.

...

methods: {
  editPost( post ) {
    this.heading = 'Edit Post'
    this.post_id = post.id
    this.post_title = post.title.rendered
    this.post_content = post.content.rendered
  },

...

}

When you save the changes to the App.vue file, the watch detects the changes and incrementally rebuilds the code. Refresh the browser, select a post from the list and verify that it is working as expected.

We need a way to Submit New Post without reloading the page. Add this markup to the bottom of the form under the submit input so we have a link that will call a newPost method.

...

    <span v-if="post_id">
        <a href="#" @click="newPost">New Post</a>
    </span>
</form>

Insert the newPost method to reset the post and heading properties to their default values.

...

methods: {
  newPost() {
    this.heading = 'Submit New Post'
    this.post_id = null
    this.post_title = null
    this.post_content = null
  },

...

}

In most cases, we should reset the form after fetching posts. Insert a call to newPosts inside the ajax callback function right after the posts object is populated with the response.

...

methods: {
  getPosts () {

  ...

    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
      this.newPost();
    }, this ))

...

}

Delete Posts

Insert this link markup in the list item element after the post title link.

...
    <a href="#" @click='delPost(post)' title="DELETE">[–]</a>
</li>

Lastly, insert the delPosts method.

...

methods: {
  delPost( post ) {
    $.ajax({
      method: "DELETE",
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts/' + post.id,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function() {
      this.getPosts();
    }, this ))
    .fail( $.proxy( function( response ) {
      console.log( response );
    }, this ));
  },

...

}

Resources

WordPress Post from Front End using REST API and Vue.js

This post is a simple proof of concept for using the new WordPress REST API to submit a new post draft from the front end. The form and user inputs are built using the Vue.js framework and vue-cli to create a simple Webpack build configuration.

Environment

This example was created using a fresh install of WordPress 4.7 served by XAMMP on Windows 10.
Nodejs version 6.9.1
NPM version 3.10.8
vue-cli version 2.6.0
git version 2.7.0

Plugin Development

Change to the plugins directory. For example,

cd c:/xampp/htdocs/wordpress/wp-content/plugins

Create the directory for the new plugin. For example,

mkdir wp-api-vuejs-poc

Entry Point

Create a plugin entry point php file in this new plugin directory. For example,

# change to the new directory
cd wp-api-vuejs-poc

# create a new file
touch wp-api-vuejs-poc.php

Add the following php code to the plugin entry point. At the very least, add the comment block at the top to register the plugin with WordPress.

wp-api-vuejs-poc.php
<?php
/**
 * Plugin Name: WP API Vue.js Proof of Concept
 * Plugin URL: http://example.com
 * Description: WordPress plugin to draft posts from the front end using the new REST API and Vue.js
 * Version: 1.0
 * Author:
 * Author URI: http://example.com
 * Text Domain: wp-api-vuejs-poc
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WP_API_Vuejs_PoC' ) ) :

/**
 * Main WP_API_Vuejs_PoC Class.
 */
final class WP_API_Vuejs_PoC {

    /**
     * The single instance of the class.
     */
    protected static $_instance = null;

    /**
     * Ensures only one instance of WP_API_Vuejs_PoC is loaded or can be loaded.
     */
    public static function instance() {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function __construct() {
        $this->define_constants();
        $this->includes();
    }

    /**
     * Define Constants.
     */
    private function define_constants() {
        $this->define( 'WAVP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
        $this->define( 'WAVP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    }

    private function define( $name, $value ) {
        if ( ! defined( $name ) ) {
            define( $name, $value );
        }
    }

    /**
     * What type of request is this?
     *
     * @param  string $type admin, ajax, cron or frontend.
     * @return bool
     */
    private function is_request( $type ) {
        switch ( $type ) {
            case 'admin' :
                return is_admin();
            case 'ajax' :
                return defined( 'DOING_AJAX' );
            case 'cron' :
                return defined( 'DOING_CRON' );
            case 'frontend' :
                return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
        }
    }

    public function includes() {
        if ( $this->is_request( 'frontend' ) ) {
            include( 'class-api-vpoc-page.php' );
        }
    }

}

endif;

/**
 * Main instance of WP_API_Vuejs_PoC.
 * Returns the main instance of WAVP to prevent the need to use globals.
 */
function WAVP() {
    return WP_API_Vuejs_PoC::instance();
}

// Global for backwards compatibility.
$GLOBALS['wp-api-vuejs-poc'] = WAVP();

Class for Front End Page Request

In the class of the plugin entry point file above, the includes function contains a request type check. When the request is made from the front end, include the class file for the page. Let’s create that class file now, for example,

touch class-api-vpoc-page.php

Add the following php code to create the class. The constructor method is called on the newly-created class object where we are subscribing to events using hooks.

The action hook is called during the page processing event for script loading, at this point, call our page_scripts function to load the app javascript.

The filter hook is called during data processing, when the content is being loaded, call our page_content function to create the app bootstrap element.

class-api-vpoc-page.php
<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WP_API_Vuejs_PoC_Page class.
 */
class WP_API_Vuejs_PoC_Page {

    /**
     * Access
     */
    private static $user_can = 'edit_posts';
    private static $page_slug = 'api-test';

    /**
     * Constructor.
     */
    public function __construct() {
        add_action( 'wp_enqueue_scripts', array( $this, 'page_scripts' ) );
        add_filter( 'the_content', array( $this, 'page_content' ) );
    }

    public function page_scripts() {
        if ( is_page( self::$page_slug ) ) {
            // load the Vue.js app
            wp_enqueue_script( 'wp-api-vuejs-poc', WAVP_PLUGIN_URL . 'dist/build.js', array(), false, true );

            // localize data for script
            wp_localize_script( 'wp-api-vuejs-poc', 'wp_api_vuejs_poc', array(
                'rest_url' => esc_url_raw( rest_url() ),
                'nonce' => wp_create_nonce( 'wp_rest' ),
                'success' => __( 'Post submitted', 'wp-api-vuejs-poc' ),
                'failure' => __( 'Post could not be processed.', 'wp-api-vuejs-poc' ),
                'current_user_id' => get_current_user_id()
                )
            );
        }
    }

    public function page_content($content) {
        if ( is_page( self::$page_slug ) ) {
            // output only to logged in users who can edit posts
            if ( is_user_logged_in() && current_user_can( self::$user_can ) ) {
                // app bootstrap element
                ob_start();?>
                <div id="app"></div>
                <?php
                $content .= ob_get_clean();
            }else{
                $content .=  sprintf( '<a href="%1s">%2s</a>', esc_url( wp_login_url() ), __( 'Log in', 'wp-api-vuejs-poc' ) );
            }
        }

        return $content;
    }

}

return new WP_API_Vuejs_PoC_Page();

The next page covers the Vue.js front end.


Laravel JWT Auth with Vue.js 2

This post is a refresh of Laravel JWT Auth with Vue.js I posted in September that applies to the 1.x version of Vue.js. Evan You released vue.js version 2 shortly thereafter and this post will cover building the Laravel JSON Web Token based authentication with it.

February 11, 2017 – A lot has changed in the last 4 months with Vue.js and Webpack. Since Vue.js 2.1.x and Webpack 2.2.x were released last month, this post originally published in November has been updated for these new versions.

Included are some notes about refactoring the application from vue.js 1.x to version 2.1. Changes also include using NPM scripts with webpack version 2 for the front end build process instead of Elixir and Browserify.

Environment

For this tutorial, Laravel 5.2 has been installed locally and it is being served by XAMMP on Windows 10.

Command Line Tools
Nodejs version 6.9.4
NPM version 3.10.10
vue-cli version 2.8.1
git version 2.10.2

User Model

After setting the database connection parameters in env.php, run the database migration script to create the users, password_resets and migrations tables. In my Laravel application, I have moved the Users class into a Models folder.

php artisan migrate

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, change the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit, config/auth.php. Update the authentication drivers user provider for the App\Models\User namespace change as follows.

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

    ...

Webpack Vue.js CLI

A simple webpack vue-loader setup is being used for the front end build process. First step is to install vue-cli globally.

npm install -g vue-cli
  • If you have an older version of vue-cli, you will need to update it to version 2.8.
npm update -g vue-cli

Using vue-cli from the laravel root directory, install the webpack-simple template into the existing laravel resources folder.

vue init webpack-simple resources

After the webpack-simple template has been installed. Change to the resources directory and run the npm install. This will install vue 2.1.x, vue-loader, vue-template-compiler, webpack 2.2.x and other dependencies. For details, inspect the install manifest resources/package.json.

cd resources

npm i
  • For example, the vue-cli 2.8.0 release from January, 2017 installs vue 2.1.10, vue-loader 10.0.2, vue-template-compiler 2.1.10 and webpack 2.2.0.

Vue Router and Components

Install the router for vue.js 2.x.

npm i vue-router --save-dev

Create a resources/assets/components folder and add an empty App.vue, Dashboard.vue, Home.vue, Register.vue and Signin.vue component.

mkdir assets/components

touch assets/components/App.vue

touch assets/components/Dashboard.vue

touch assets/components/Home.vue

touch assets/components/Register.vue

touch assets/components/Signin.vue

Add the following code to the App.vue base layout component. The new <router-link> component replaces the the deprecated v-link directive from vue.js 1.x.

App.vue
<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">Home</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'register' }">Register</router-link>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Add the following code to the Home.vue component.

Home.vue
<template>
    <h1>Laravel 5</h1>
</template>

Add the following code to the Register.vue component. Note the change from version 1.0 of this component. A div has been added at the root of the template since components now must have exactly one root element.

Register.vue
<template>
    <div>
        <div class="alert alert-danger" v-if="error && !success">
            <p>There was an error, unable to complete registration.</p>
        </div>
        <div class="alert alert-success" v-if="success">
            <p>Registration completed. You can now sign in.</p>
        </div>
        <form autocomplete="off" v-on:submit="register" v-if="!success">
            <div class="form-group" v-bind:class="{ 'has-error': error && response.username }">
                <label for="name">Name</label>
                <input type="text" id="name" class="form-control" v-model="name" required>
                <span class="help-block" v-if="error && response.name">{{ response.name }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.email }">
                <label for="email">E-mail</label>
                <input type="email" id="email" class="form-control" placeholder="gavin.belson@hooli.com" v-model="email" required>
                <span class="help-block" v-if="error && response.email">{{ response.email }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.password }">
                <label for="password">Password</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
                <span class="help-block" v-if="error && response.password">{{ response.password }}</span>
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
        </form>
    </div>
</template>

JavaScript

Create a resources/assets/js folder for the JavaScript.

mkdir assets/js

Create a resources/assets/js/app.js JavaScript file.

touch assets/js/app.js

Add the following code to the app.js file to import vue modules, components and define the router. Exporting will allow other modules in this project to import them.

Note the changes between version 1.0 of app.js.

  1. router.map has been replaced with an array on the new routes option.
  2. router is passed to the new Vue instance as option since starting an app with routing no longer requires a special method.
app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from '../components/App.vue';
import Dashboard from '../components/Dashboard.vue';
import Home from '../components/Home.vue';
import Register from '../components/Register.vue';
import Signin from '../components/Signin.vue';

Vue.use(VueRouter);

export default Vue;

export var router = new VueRouter({
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
        {
            path: '/register',
            name: 'register',
            component: Register
        }
    ]
});

new Vue({
    el: '#app',
    router: router,
    render: app => app(App)
});

Web Page

Edit the resources/views/welcome.blade.php template.

Replace the entire contents of the file with this markup.

welcome.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{!! csrf_token() !!}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
    <div class="container">
        <div id="app"></div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>

Webpack

Edit the resources/webpack.config.js entry and output values.

REPLACE
module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  ...
}
WITH
module.exports = {
  entry: './assets/js/app.js',
  output: {
    path: path.resolve(__dirname, '../public/js'),
    publicPath: '/js/',
    filename: 'app.js'
  },
  ...
}

Test drive the updated webpack configuration with the npm build script.

npm run build

Your build output should be similar to this. The laravel public folder should now contain js/app.js and a respective js/app.js.map source map.

> resources@ build C:\xampp\htdocs\laravel\resources
> cross-env NODE_ENV=production webpack --progress --hide-modules

Hash: 260fd0b01225d3f55a16
Version: webpack 2.2.1
Time: 6031ms
     Asset    Size  Chunks             Chunk Names
    app.js  118 kB       0  [emitted]  main
app.js.map  884 kB       0  [emitted]  main
Laravel JWT Auth Vue.js 2.0 Home Page

The next page covers Vue Resource for request handling, Form Request Validation, API authorization with JWT Auth, User endpoint and Sign in.


Laravel JWT Auth with Vue.js

This post documents using Laravel to build JSON Web Token based authentication with a Vue.js 1.0 user interface. UPDATE: vue.js 2.0 version published.

A fresh install of Laravel 5.2 for local development is required. Head over to the Laravel 5.2 docs to get setup with Composer and Laravel as needed. This post contains some info on how I installed Laravel 5.2 on Windows. Once Laravel 5.2 is installed, the database created and the .env file updated to connect to it, run the migration to create the users table.

php artisan migrate

User Model

One of the first things I like to do is create a Models folder and move the User model into it. To organize models within their own directory, start by creating a folder named Models in the app directory and move the User model into it.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, change the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit, config/auth.php. Update the authentication drivers user provider for the App\Models\User namespace change as follows.

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

    ...

Laravel Elixir Vueify

Using NPM, install the dependencies including laravel-elixer and gulp that should already be defined in the package.json file.

npm install

Install the Laravel Elixir and the Browserify Vueify plugin wrapper, laravel-elixir-vueify. This node package also includes vue.js.

For Laravel Elixer 5
npm install laravel-elixir-vueify@1.0.6 --save-dev

Laravel Elixir is a build tool wrapper for Gulp. To use laravel-elixir-vueify, require laravel-elixir-vueify and update the elixir call with mix.browserify in the gulpfile.js as follows.

REPLACE
elixir(function(mix) {
    mix.sass('app.scss');
});
WITH
require('laravel-elixir-vueify');

elixir(function(mix) {
    mix.browserify('app.js');
    mix.sass('app.scss');
});

Create a resources/assets/js folder for the JavaScript.

mkdir resources/assets/js

Create a resources/assets/js/app.js JavaScript file.

touch resources/assets/js/app.js

Test the gulp build. A browserified app.js file that includes an external source map file should get compiled and written to public/js/app.js. The pre-existing app.scss Sass file should also get compiled.

gulp
Default Gulp task output
Default Gulp task output

Vue Router and Components

Install the router for Vue.js.

npm install vue-router@0.7.13 --save-dev
  • The npm install vue-router command above has been updated to install the version supported by the 1.x version of vue.js. This is the vue.js version installed with laravel-elixir-vueify@1.0.6.

Create a resources/assets/components folder and add an empty App.vue, Dashboard.vue, Home.vue, Register.vue and Signin.vue component.

mkdir resources/assets/components

touch resources/assets/components/App.vue

touch resources/assets/components/Dashboard.vue

touch resources/assets/components/Home.vue

touch resources/assets/components/Register.vue

touch resources/assets/components/Signin.vue

Add the following code to the empty App.vue file which will be the base layout component.

App.vue
<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li><a v-link="{ name: 'home' }">Home</a></li>
                    <li class="pull-right">
                        <a v-link="{ name: 'register' }">Register</a>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Add the following code to the empty Home.vue file. The content in this template will populate the App.vue router-view element by default.

Home.vue
<template>
    <h1>Laravel 5</h1>
</template>

Add the following code to the empty Register.vue file for the user account creation form.

Register.vue
<template>
    <div class="alert alert-danger" v-if="error && !success">
        <p>There was an error, unable to complete registration.</p>
    </div>
    <div class="alert alert-success" v-if="success">
        <p>Registration completed. You can now sign in.</p>
    </div>
    <form autocomplete="off" v-on:submit="register" v-if="!success">
        <div class="form-group" v-bind:class="{ 'has-error': error && response.username }">
            <label for="name">Name</label>
            <input type="text" id="name" class="form-control" v-model="name" required>
            <span class="help-block" v-if="error && response.name">{{ response.name }}</span>
        </div>
        <div class="form-group" v-bind:class="{ 'has-error': error && response.email }">
            <label for="email">E-mail</label>
            <input type="email" id="email" class="form-control" placeholder="gavin.belson@hooli.com" v-model="email" required>
            <span class="help-block" v-if="error && response.email">{{ response.email }}</span>
        </div>
        <div class="form-group" v-bind:class="{ 'has-error': error && response.password }">
            <label for="password">Password</label>
            <input type="password" id="password" class="form-control" v-model="password" required>
            <span class="help-block" v-if="error && response.password">{{ response.password }}</span>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
    </form>
</template>

Add the following code to the empty app.js file to require vue, the vue-router, import vue components, setup the router. Export Vue and the router to allow modules to import them.

app.js
var Vue = require('vue');
var VueRouter = require('vue-router');

import App from '../components/App.vue';
import Dashboard from '../components/Dashboard.vue';
import Home from '../components/Home.vue';
import Register from '../components/Register.vue';
import Signin from '../components/Signin.vue';

Vue.use(VueRouter);

export default Vue;
export var router = new VueRouter

router.map({
    '/': {
        name: 'home',
        component: Home
    },
    '/register': {
        name: 'register',
        component: Register
    }
});

router.start(App, '#app');

Web Page

Edit the resources/views/welcome.blade.php template.

Replace the entire contents of the file with this markup.

welcome.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{!! csrf_token() !!}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
    <div class="container">
        <div id="app"></div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>

Now is a good time to test that everything builds and you have a Bootstrap styled page with with links to Home and Register in a horizontal navigation bar and a content panel under it with “Laravel 5” heading text. Select the Register link to diplay the form in the content panel.

gulp
Laravel JWT Auth Vuejs User Registration Form
New User Registration Form

The next page covers Vue Resource for request handling, Form Request Validation, API authorization with JWT Auth, User endpoint and Sign in.


Handlebars Templates with Browserify

This post documents how to use the hbsfy precompiler plugin for Browserify to compile Handlebars templates into javascript. With this design, templates can be required as javascript modules in the views that use them.

Requirements

Global Packages

List the global packages that are installed with the npm list –global command. Then install these if they are not already installed or need to be updated.

Install Browserify

After installing Node.js, using a Command shell, install Browserify globally. *

# global browserify install
npm install -g browserify

* If you have an older version of global Browserify, run npm rm –global browserify to remove the old version before installing the newer one.

Install Gulp

Install gulp globally with the npm install -g command as follows: *

# global gulp install
npm install -g gulp

* If you have an older version of global Gulp, run npm rm –global gulp to remove the old version before installing the newer one.

Project Packages

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm.

# create package.json
npm init

Install Packages

These are the project node module packages needed to browserify and bundle the Handlebars template modules. Since all of the modules are bundled into a single minified javascript file, source maps are included in the bundle task to make it easier to debug in the browser’s developer tools. For convenience, a local web server module has been included.

$ npm install browserify --save-dev
$ npm install handlebars --save-dev
$ npm install hbsfy --save-dev
$ npm install gulp --save-dev
$ npm install gulp-sourcemaps --save-dev
$ npm install gulp-uglify --save-dev
$ npm install gulp-util --save-dev
$ npm install gulp-webserver --save-dev
$ npm install vinyl-source-stream --save-dev
$ npm install vinyl-buffer --save-dev

Create a gulpfile.js in the project root to define and configure the Gulp tasks. The default task is simply gulp. When gulp is executed, it will run the browserify bundle task, start a local web server and open the default web page in a browser.

gulpfile.js
'use strict';

var gulp        = require('gulp'),
    browserify  = require('browserify'),
    buffer      = require('vinyl-buffer'),
    gutil       = require('gulp-util'),
    source      = require('vinyl-source-stream'),
    sourcemaps  = require('gulp-sourcemaps'),
    uglify      = require('gulp-uglify'),
    webserver   = require('gulp-webserver');

var hbsfy = require('hbsfy');

gulp.task('bundle', function () {
  var b = browserify({
    entries: './src/js/index.js',
    debug: true
  });

  return b.transform(hbsfy).bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
        .pipe(uglify())
        .on('error', gutil.log)
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('./js/'));
});

// static web server
gulp.task('server', function() {
    gulp.src('./')
        .pipe(webserver({
            port: 3000,
            directoryListing: false,
            open: true
        }));
});

gulp.task('default', ['bundle', 'server']);

Source

Create a Handlebars template in /src/hbs/

people.hbs
{{#each people}}
<li>{{firstName}} {{lastName}}</li>
{{/each}}

Create a data module in /src/js/modules/ for this template demo since we are not calling a rest API to get data.

data.js
module.exports = {
    people: [
        {firstName: "Yehuda", lastName: "Katz"},
        {firstName: "Carl", lastName: "Lerche"},
        {firstName: "Alan", lastName: "Johnson"}
    ]
}

In the /src/js folder, create the main node, index.js.

index.js
var data = require('./modules/data');
var list = require('../hbs/people.hbs');

document.querySelector('#people').innerHTML = list(data);

Web Page

Create this html file to load the javascript and render the template output.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Handlebars with Browserify</title>
</head>
<body>
    <ul id="people"></ul>
    <script src="js/bundle.js"></script>
</body>
</html>

Project Folders and Files

Here is a tree view of the projects folders and files. Browse, clone or download the source code if you prefer.

    • js
    • src
      • hbs
        • people.hbs
      • js
        • modules
          • data.js
        • index.js
    • gulpfile.js
    • package.json
    • index.html

The next page covers custom Handlebars helper registration


Laravel User Registration with Email Activation

This post documents how to add an e-mail confirmation to the Laravel User registration that is generated by the Artisan console. In this example, the registered user will not be able to login until the activation link that is sent to the users registered e-mail account has been loaded into the browser.

Requirements

An instance of Laravel 5.2 setup for local development with authentication scaffolding is required. Refer to the first page of my recent post on Laravel User Authentication with Ajax Validation for more information on installing Laravel 5.2 using Composer and generating the authentication scaffolding with artisan.

User Model

I like placing model classes in their own directory. This can be done with just a few minor changes after moving the User class into a new app/Models folder.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, update the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, checkout the email-activation branch in the source code.

Edit app/Http/Controllers/Auth/AuthController.php, update use App\User to use App\Models\User.

AuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    ...

Edit, config/auth.php, update the authentication drivers user provider.

'users' => [
    'driver' => 'eloquent',
    'model' => App\User::class,
],
REPLACE
'model' => App\User::class,
WITH
'model' => App\Models\User::class,

Database Migration Files

Generate a database migration file for creating the user_activations table. This table stores the activation codes until they are used.

create_user_activations_table
php artisan make:migration create_user_activations_table --create=user_activations

Edit the migration file up function to create these three fields in the user_activations table.

public function up()
{
    Schema::create('user_activations', function (Blueprint $table) {
        $table->integer('user_id')->unsigned();
        $table->string('token')->index();
        $table->timestamp('created_at');
    });
}
alter_users_table_add_activated_col

Generate a database migration file for adding a column to the users table.

php artisan make:migration alter_users_table_add_activated_col

Edit the migration file up function to add the activated field to the users table.

public function up()
{
    Schema::table('users', function($table) {
        $table->boolean('activated')->default(false);
    });
}

Run the migration

php artisan migrate

ActivationRepository Class

Create a Repositories folder in the app directory

mkdir app/Repositories

Create the ActivationRepository class file in app/Repositories. This class contains functions for generating activation tokens and updating the tables in the database.

ActivationRepository.php
<?php

namespace App\Repositories;

use Carbon\Carbon;
use Illuminate\Database\Connection;

class ActivationRepository
{
    protected $db;
    protected $table = 'user_activations';

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    public function getActivation($user)
    {
        return $this->db->table($this->table)->where('user_id', $user->id)->first();
    }

    public function getActivationByToken($token)
    {
        return $this->db->table($this->table)->where('token', $token)->first();
    }

    public function deleteActivation($token)
    {
        $this->db->table($this->table)->where('token', $token)->delete();
    }

    public function createActivation($user)
    {
        $activation = $this->getActivation($user);

        if (!$activation) {
            return $this->createToken($user);
        }
        return $this->regenerateToken($user);
    }

    protected function getToken()
    {
        return hash_hmac('sha256', str_random(40), config('app.key'));
    }

    private function regenerateToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->table)->where('user_id', $user->id)->update([
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    private function createToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->table)->insert([
            'user_id' => $user->id,
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }
}

ActivationFactory Class

Create a Factories folder in the app directory

mkdir app/Factories

Create the ActivationFactory class file in app/Factories.

ActivationFactory.php
<?php

namespace App\Factories;

use App\Models\User;
use App\Repositories\ActivationRepository;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;

class ActivationFactory
{
    protected $activationRepo;
    protected $mailer;
    protected $resendAfter = 24;

    public function __construct(ActivationRepository $activationRepo, Mailer $mailer)
    {
        $this->activationRepo = $activationRepo;
        $this->mailer = $mailer;
    }

    public function sendActivationMail($user)
    {
        if ($user->activated || !$this->shouldSend($user)) {
            return;
        }

        $token = $this->activationRepo->createActivation($user);

        $link = route('user.activate', $token);
        $message = sprintf('Activate account %s', $link, $link);

        $this->mailer->raw($message, function (Message $m) use ($user) {
            $m->to($user->email)->subject('Activation mail');
        });
    }

    public function activateUser($token)
    {
        $activation = $this->activationRepo->getActivationByToken($token);

        if ($activation === null) {
            return null;
        }

        $user = User::find($activation->user_id);

        $user->activated = true;

        $user->save();

        $this->activationRepo->deleteActivation($token);

        return $user;
    }

    private function shouldSend($user)
    {
        $activation = $this->activationRepo->getActivation($user);
        return $activation === null || strtotime($activation->created_at) + 60 * 60 * $this->resendAfter < time();
    }
}

Route

Create a route for the e-mail activation link in app/Http/routes.php. This user.activate route should be added after Route::auth()

routes.php
<?php
...
Route::auth();

Route::get('user/activation/{token}', 'Auth\AuthController@activateUser')->name('user.activate');

AuthController

Make the following updates to app/Http/Controllers/Auth/AuthController.php

REPLACE
use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
WITH
use App\Factories\ActivationFactory;
use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;
REPLACE
public function __construct()
{
    $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
}
WITH
protected $activationFactory;

public function __construct(ActivationFactory $activationFactory)
{
    $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
    $this->activationFactory = $activationFactory;
}

At the bottom of the controller, add these three functions after the create function. The register function we are adding will override the register method inherited from the AuthenticatesAndRegistersUsers trait.

...

public function register(Request $request)
{
    $validator = $this->validator($request->all());

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    $user = $this->create($request->all());

    $this->activationFactory->sendActivationMail($user);

    return redirect('/login')->with('activationStatus', true);
}

public function activateUser($token)
{
    if ($user = $this->activationFactory->activateUser($token)) {
        auth()->login($user);
        return redirect($this->redirectPath());
    }
    abort(404);
}

public function authenticated(Request $request, $user)
{
    if (!$user->activated) {
        $this->activationFactory->sendActivationMail($user);
        auth()->logout();
        return back()->with('activationWarning', true);
    }
    return redirect()->intended($this->redirectPath());
}

Authentication Language Lines

For the activation status messages that we need to display to the user, update the resources/lang/en/auth.php file.

en/auth.php
<?php

return [

...

    'activationStatus' => 'We sent you an activation code. Please check your e-mail.',
    'activationWarning' => 'You need to confirm your account. We have sent you an activation code, please check your e-mail.',

...

];

Add the following inside the form at resources/views/auth/login.blade.php Blade view to render the activation messages.

login.blade.php
...

@if (session('activationStatus'))
    <div class="alert alert-success">
        {{ trans('auth.activationStatus') }}
    </div>
@endif

@if (session('activationWarning'))
    <div class="alert alert-warning">
        {{ trans('auth.activationWarning') }}
    </div>
@endif

Source Code

Laravel User Authentication with Ajax Validation

This post documents how to add Ajax form validation to Laravel User Authentication that is generated by the Artisan console.

Requirements

An instance of Laravel 5.2 setup for local development is required. Head over to the Laravel 5.2 docs to get setup as needed. For my setup on Windows 10, I am using XAMPP 5.6.12 which meets all of the requirements. Note that your document root will need to be the laravel/public directory. Here is the virtual hosts configuration I am using for Laravel.

httpd-vhosts.conf
<VirtualHost *:8080>
    DocumentRoot "C:/xampp/htdocs/laravel/public"
    ServerName laravel.dev
    ServerAlias www.laravel.dev
    SetEnv APPLICATION_ENV development
    <Directory "C:/xampp/htdocs/laravel">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

More info on setting up XAMPP for Windows

New Laravel Site

Using Composer, install Laravel 5.2 into htdocs/laravel

cd c:/xampp/htdocs

composer create-project laravel/laravel laravel "5.2.*"
Generate Authentication Scaffolding
cd laravel

php artisan make:auth

Now the Laravel site has routes and views to allow a user to register, login, logout, and reset their password. A HomeController has also been added to allow authenticated users to enter its view.

User Model

I prefer to organize models within their own directory. Therfore, let’s create a folder named Models in the app directory and move the User model into it.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, update the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit app/Http/Controllers/Auth/AuthController.php, update use App\User to use App\Models\User.

AuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    ...

Edit, config/auth.php, update the authentication drivers user provider.

'users' => [
    'driver' => 'eloquent',
    'model' => App\User::class,
],
REPLACE
'model' => App\User::class,
WITH
'model' => App\Models\User::class,

Database

Edit the .env configuration file to access the database. I have already created a database named laravel on the MySQL Server that is part of XAMPP.

.env
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

The settings above are the only ones I needed to update for my setup, I do not have a password set for MySQL Server and will just use the root account for local development. Now run php artisan migrate to create the tables for users.

Run Migration
php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

Validation Rules

The validation rules are currently located in the AuthController, I would like to be able to share them with the new controller we will create later for ajax validation. Since these rules apply to the User, and that model is already being used by the AuthController, it seeems to be the best place for them.

Edit app/Models/User.php, adding the public function rules() to the class.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * Get the validation rules that apply to the user.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6|confirmed',
        ];
    }

    ...

Edit app/Http/Controllers/Auth/AuthController.php. Locate the validator function and replace the array of rules being passed to the Validator make method with a call to the newly created rules method in our User model.

REPLACE
return Validator::make($data, [
    'name' => 'required|max:255',
    'email' => 'required|email|max:255|unique:users',
    'password' => 'required|min:6|confirmed',
]);
WITH
return Validator::make($data, (new User)->rules());

Before going further, now would be a good time to test registering a new user, login, logout etc.. to make sure everyting works. All we have done so far aside from the code that artisan generated is move the User model and move the validation rules from the AuthController into it.

The next page covers using the api middleware in routes, creating the Validation\UserController for handling the XHR request and finally updating the templates and adding the JavaScript for Ajax.


Google Maps API with Browserify

This post documents how to use local JavaScript modules with Browserify and the Google Maps JavaScript API. In this example, the Google Map contains a marker that can be dragged to reset the browser form with the marker position location data. Additionally, Browserify shim is used to require jQuery since it is already being loaded from a CDN.

Requirements

Install Browserify

After installing Node.js, using a Command shell, install Browserify globally. *

# global browserify install
npm install -g browserify

* If you have an older version of global Browserify, run npm rm –global browserify to remove the old version before installing the newer one.

Project Folders and Files

Create these folders and files. Browse, clone or download the source code if you prefer.

    • css
      • style.css
    • js
    • src
      • js
        • modules
          • gapi.js
          • gmap.js
          • jsonp.js
          • location.js
        • index.js
    • gulpfile.js
    • index.html

Web Page

This web page contains the map container, address and geocode location inputs. jQuery is being loaded from the Google Hosted Libraries CDN.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<title>Page Title</title>
	<link rel="stylesheet" href="/css/style.css">
</head>
<body>
	
	<p>Drag the marker to a location on the map and set input values.</p>
	
	<div id="map-canvas"></div>
	<label>Street
		<input type="text" id="street" name="street" />		
	</label>
	<label>Locality
		<input type="text" id="locality" name="locality" />
	</label>
	<label>Region
		<input type="text" id="region" name="region" />
	</label>
	<label>Country
		<input type="text" id="country" name="country" />
	</label>
	<label>Latitude
		<input type="text" id="latitude" name="latitude" />
	</label>
	<label>Longitude
		<input type="text" id="longitude" name="longitude" />
	</label>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>

        <!-- Prod / built version -->
	<!--script src="/js/bundle.js"></script-->

</body>
</html>

Application Entry Point

The index module is the starting point for the map application. The module dependency requirements are defined followed by a call to the load function exported by the gapi module. gapi.load uses a callback function before the location.init.

index.js
var $ = require('jQuery');
var gapi = require('./modules/gapi');
var location = require('./modules/location');

gapi.load( function () {
    location.init();
});

Google Map JavaScript API

Loading the Google Maps JavaScript API requires a global callback. For this I have created a jsonp module which accepts three parameters, a URL (url), a global callback function name (callbackname), and a callback function (done).

jsonp.js
module.exports = function (url, callbackname, done) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(script, s);

    window[callbackname] = done;
};

The gapi module makes use of the jsonp module to load the API:

gapi.js
var jsonp = require('./jsonp');

var gapiurl = 'http://maps.googleapis.com/maps/api/js?callback=__googleMapsApiOnLoadCallback';

exports.load = function (done) {
    jsonp(gapiurl, '__googleMapsApiOnLoadCallback', done);
};

Location Module

The location module initiates the google map using the gmap.init function exported by the gmap module. The gmap.init callback results data is handled by the location module to populate the address inputs.

location.js
var gmap = require('./gmap');

var latitude = 39.084014922903;
var longitude = -77.51372591791;

exports.init = function () {
    gmap.init(
        latitude,
        longitude,
        function(result){
            console.log('RESULT',result);

            if (result.address) {
                _setAddress(result.address);
            }

            $('#latitude').val(result.latitude);
            $('#longitude').val(result.longitude);
        }
    );
};

_setAddress = function(address) {
    var street = [];
    if (address.street_number) {
        street.push(address.street_number);
    }
    if (address.route) {
        street.push(address.route);
    }
    if (street.length > 0) {
        $('#street').val(street.join(' '));
    }
    if (address.locality) {
        $('#locality').val(address.locality);
    }
    if (address.administrative_area_level_1) {
        $('#region').val(address.administrative_area_level_1);
    }
    if (address.country) {
        $('#country').val(address.country);
    }
};

Google Map

The gmap module uses the Google Maps JavaScript API.

gmap.js
exports.init = function (lat, lng, callback) {
    var geocoder = new google.maps.Geocoder();
    var marker = new google.maps.Marker({
        position: new google.maps.LatLng(lat, lng),
        anchorPoint: new google.maps.Point(0, -29),
        draggable: true
    });
    map = new google.maps.Map(document.getElementById('map-canvas'), {zoom: 10});
    map.setCenter(marker.position);
    marker.setMap(map);

    google.maps.event.addListener(marker, 'dragend', function (evt) {

        var latlng = {lat: evt.latLng.lat(), lng: evt.latLng.lng()};
        var addrComponents = {
            street_number: 'short_name',
            route: 'long_name',
            establishment: 'long_name',
            locality: 'long_name',
            administrative_area_level_1: 'short_name',
            country: 'short_name',
            postal_code: 'short_name'
        };
        result = {
            address: {},
            latitude: 0,
            longitude: 0
        };

        geocoder.geocode({'location': latlng}, function(results, status) {
            if (status === google.maps.GeocoderStatus.OK) {
                if (results[0]) {

                    var i; var type = null;
                    for (i = 0; i < results[0].address_components.length; i++) {
                        type = results[0].address_components[i].types[0];
                        if (addrComponents[type]) {
                            result.address[type] = results[0].address_components[i][addrComponents[type]];
                        }
                    }
                    result.latitude = latlng.lat;
                    result.longitude = latlng.lng;
                    if(typeof callback == "function"){
                        callback(result);
                    }

                } else {
                    window.alert('No results found');
                }
            } else {
                window.alert('Geocoder failed due to: ' + status);
            }
        });
    });
}
style.css

Add this file into the css folder to declare some basic styles.

label {
	display: block;
	width: 300px;
	margin-bottom: 1em;
}
input[type='text'] {
	display: block;
	width: 100%;
}
#map-canvas {
	width: 300px;
	height: 300px;
	margin-bottom: 1em;
}

Build

Use the Gulp task and build runner to configure and run the build process. Install Gulp and the Node modules locally that are needed to properly bundle, minify and sourcemap the Browserify modules.

Install Gulp

Install gulp globally with the npm install -g command as follows: *

# global gulp install
npm install -g gulp

* If you have an older version of global Gulp, run npm rm –global gulp to remove the old version before installing the newer one.

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm.

# create package.json
npm init

Install node modules

# install dependencies and save to package.json
npm install browserify --save-dev
npm install browserify-shim --save-dev
npm install gulp --save-dev
npm install gulp-sourcemaps --save-dev
npm install gulp-uglify --save-dev
npm install gulp-util --save-dev
npm install gulp-webserver --save-dev
npm install vinyl-buffer --save-dev
npm install vinyl-source-stream --save-dev

Browserify Shim

Browserify shim is used so global jQuery can be required by modules. Near the bottom of the package.json file, add these browserify and browserify-shim properties. View the source code if needed.

"browserify": {
  "transform": ["browserify-shim"]
},
"browserify-shim": {
  "jQuery": "global:jQuery"
}

Create a gulpfile.js in the project root to define and configure the Gulp tasks. The default task is simply gulp. When gulp is executed, it will run the bundle task, start a local web server with livereload and open the app in a web browser.

gulpfile.js
'use strict';

var gulp        = require('gulp'),
    browserify  = require('browserify'),
    buffer      = require('vinyl-buffer'),
    gutil       = require('gulp-util'),
    source      = require('vinyl-source-stream'),
    sourcemaps  = require('gulp-sourcemaps'),
    uglify      = require('gulp-uglify'),
    webserver   = require('gulp-webserver');

gulp.task('bundle', function () {
  var b = browserify({
    entries: './src/js/index.js',
    debug: true
  });

  return b.bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
        .pipe(uglify())
        .on('error', gutil.log)
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('./js/'));
});

// static web server w/ livereload
gulp.task('server', function() {
    gulp.src('./')
        .pipe(webserver({
            livereload: true,
            directoryListing: false,
            open: true
        }));
});

gulp.task('default', ['bundle','server']);

Add watchify to the gulp workflow on the next page.

Source Code

Xdebug for XAMPP on OS X

After discovering how nice the vscode-php-debug extension works in Visual Studio Code on my Windows 10 laptop, I wanted this same setup for a PHP project on my work Mac Book Pro running OS X Yosemite (10.10). The version of XAMPP I currently have installed on the Mac is 5.6.11-0 and for this tutorial, there are a few requirements to consider:

Requirements

Use homebrew to install the autoconf utility for producing configure scripts.

# install autoconf
brew install autoconf

phpize

XAMPP comes with phpize which we will want to utilize to prepare the build environment for the Xdebug PHP extension. If another version of phpize is installed, it will need to be renamed. Check to see if phpize is installed in /usr/bin:

cd /usr/bin
ls -al | grep phpize
-rwxr-xr-x     1 root   wheel      4508 Sep  9  2014 phpize
  • Starting with OS X 10.11 El Capitan, system files and processes are protected with the new System Integrity Protection feature. Therefore, the following writes to /usr/bin are not permitted unless SIP is disabled (not recommended). For a workaround, try adding this environment variable to the .bash_profile: export PATH=/Applications/XAMPP/xamppfiles/bin:$PATH

If the grep search returns phpize, similar to what is shown above, then rename it to phpize_bak.

# rename / backup phpize
sudo mv phpize phpize_bak

Create a new symbolic link in /usr/bin to target the XAMPP version of phpize.

# navigate to the /usr/bin directory
cd /usr/bin

# create symbolic link to XAMPP phpize
sudo ln -s /Applications/XAMPP/bin/phpize-5.6.11 phpize

# check phpize version
cd /
phpize -v
Configuring for:
PHP Api Version:         20131106
Zend Module Api No:      20131226
Zend Extension Api No:   220131226

phpize -v command output should be similar to what is shown above.

Xdebug Installation

Open the XAMPP phpinfo.php file in a web browser, for example, http://localhost/dashboard/phpinfo.php. In another browser window or tab, open https://xdebug.org/wizard.php and copy the phpinfo page content in the first window or tab and paste it into the textbox on the xdebug.org page. Then submit for analysis to determine the Xdebug source to download. The response should be a Summary and Tailored Installation Instructions.

Example Summary
  • Xdebug installed: no
  • Server API: Apache 2.0 Handler
  • Windows: no
  • Zend Server: no
  • PHP Version: 5.6.11
  • Zend API nr: 220131226
  • PHP API nr: 20131226
  • Debug Build: no
  • Thread Safe Build: no
  • Configuration File Path: /Applications/XAMPP/xamppfiles/etc
  • Configuration File: /Applications/XAMPP/xamppfiles/etc/php.ini
  • Extensions directory: /Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226
Example Instructions
  1. Download xdebug-2.4.0.tgz
    continued…

You can follow the Tailored Installation Instructions from xdebug.org or you can follow the instructions here that I have found to work well.

  1. Unpack the downloaded file
    # navigate to the downloaded file
    cd ~/Downloads
    
    tar -xvzf xdebug-2.4.0.tgz
    cd xdebug-2.4.0
    
  2. run phpize
    phpize
    
    # example output
    Configuring for:
    PHP Api Version:         20131106
    Zend Module Api No:      20131226
    Zend Extension Api No:   220131226
    
  3. Configure with XAMPP php-config.
    ./configure --enable-xdebug --with-php-config=/Applications/XAMPP/xamppfiles/bin/php-config
    
  4. run make
    make
  5. In the Tailored Installation Instructions from xdebug.org, locate the step after make that contains the cp command and paths for copying the xdebug.so extension file into XAMPP, for example,
    cp modules/xdebug.so /Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226
    
  6. Edit /Applications/XAMPP/xamppfiles/etc/php.ini
    After “;zend_extension”
    Add the zend_extension setting from the xdebug.org instructions, for example:
    zend_extension=/Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so
    
  7. Additionally, add these xdebug configuration settings in php.ini after the zend_extension setting:
    xdebug.remote_enable=1
    xdebug.remote_handler=dbgp
    xdebug.remote_host=localhost
    xdebug.remote_autostart = 1
    xdebug.remote_port=9000
    xdebug.show_local_vars=1
    xdebug.remote_log=/Applications/XAMPP/logs/xdebug.log
    
    ;---------------------------------
    ; uncomment these dev environment
    ; specific settings as needed
    ;---------------------------------
    ;xdebug.idekey = "netbeans-xdebug"
    ;xdebug.idekey = "sublime.xdebug"
    
    ;some pages in your Drupal site will not work default = 100
    ;xdebug.max_nesting_level=256
    
    Optional – for development only
    Find “max_execution_time” and set to unlimited:
    max_execution_time=0
    
  8. Restart Apache using XAMPP’s manager-osx
  9. Reload http://localhost/dashboard/phpinfo.php and verify that xdebug section exists. Another verification method is to copy and paste the phpinfo into the form at https://xdebug.org/wizard.php and resubmit it for analysis.

Customizing Media Manager

These examples show how to load copies of com_media views and templates instead of their core counterparts for customization without hacking any core Joomla files.

Here is the secret sauce to override com_media views and templates. A system plugin with an onAfterInitialise() event handler to capture request data and evaluate parameters using JInput. For example,

/plugins/system/mcm/mcm.php
// no direct access
defined ( '_JEXEC' ) or die ( 'Restricted access' );

jimport('joomla.plugin.plugin');

class plgSystemMcm extends JPlugin { 

    public function onAfterInitialise() {

        $input = JFactory::getApplication()->input;

        if('com_media' == $input->getCMD('option')) {

            $this->loadView($input->getCMD('view'));

        }

        return true;
    }

    protected function loadView($view)
    {
        if (('images' == $view) || ('imagesList' == $view)) {

            $overridePath = FOFPlatform::getInstance()->getTemplateOverridePath('com_media', true) . '/' . $view;

            require_once $overridePath . '/view.html.php';
        }
    }
}

Application Specific onAfterInitialise()

This version of the onAfterInitialise() function shows how to apply the override for just the frontend (site) views and templates.

public function onAfterInitialise() {

    $app = JFactory::getApplication();

    if ($app->isSite()) {

        if('com_media' == $app->input->getCMD('option')) {

            $this->loadView($app->input->getCMD('view'));

        }

        return true;
    }

    return false
}

Create the new custom templates and views by copying the folders from the com_media component into your template folders. For example, if you want to customize media manager for your protostar template, copy the com_media folders into the protostar template as follows:

  1. copy
    /administrator/components/com_media/views/images
    to
    /templates/protostar/html/com_media/images
  2. copy
    /administrator/components/com_media/views/imageslist
    to
    /templates/protostar/html/com_media/imageslist

Directory structure of com_media protostar template overrides.

  • templates
    • protostar
      • html
        • com_media
          • images
            • tmpl
              • default.php
          • view.html.php
          • imageslist
            • tmpl
              • default_folder.php
              • default_image.php
              • default.php
          • view.html.php

Then modify both copies of view.html.php to include their respective default.php template copies.

At the end of the display function, comment out or replace,
parent::display($tpl);

with an include directive to the template copy,
include( dirname(__FILE__) . '/tmpl/default.php');

for example, in both the images and imageslist folders:

/templates/protostar/html/com_media/images/view.html.php
/templates/protostar/html/com_media/imageslist/view.html.php
//parent::display($tpl);
include( dirname(__FILE__) . '/tmpl/default.php');

Also comment or replace both loadTemplate('folder') and loadTemplate('image') function calls in the imageslist default.php template and include their file and folder default.php template copies.

for example,

/templates/protostar/html/com_media/imageslist/tmpl/default.php
<?php for ($i = 0, $n = count($this->folders); $i < $n; $i++) :
	$this->setFolder($i);
	//echo $this->loadTemplate('folder');
	include( dirname(__FILE__) . '/default_folder.php');
endfor; ?>

<?php for ($i = 0, $n = count($this->images); $i < $n; $i++) :
	$this->setImage($i);
	//echo $this->loadTemplate('image');
	include( dirname(__FILE__) . '/default_image.php');
endfor; ?>

The com_media views and templates will now be loaded instead of their core counterparts and can be customized without hacking any core Joomla files. The plugin code above can be found here on GitHub. The next page covers extending the com_media controller to handle base64 image uploads.

source code

Google Maps API RequireJS Module

This post documents how to create a RequireJS module that loads the Google Maps JavaScript API and is used by a web page to populate form inputs with location data. The Google Map contains a marker that can be dragged to reset the form with the marker position location data.

source code

Development Environment

In order to use the package managers, Gulp task runner, BrowserSync and RequireJS optimizer, you will need to have the following installed.

  1. Node.js
  2. Bower

Using a Command shell in the project root, follow these steps to get the local development environment setup.

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm. This is useful to have when you want to re-install or update the dependencies with the npm install command.

# create package.json
npm init

Install node modules

# install dependencies and save to package.json
npm install browser-sync --save-dev
npm install gulp --save-dev
npm install requirejs --save-dev

Create bower.json

The bower.json file contains project metadata and lists dependencies that are available on bower. Re-install / update bower components saved to this file with the bower install command.

# create bower.json
bower init

Install bower components

Only using the async plugin from this requirejs-plugins bundle for async dependency loading of the Google Maps JavaScript API.

# install bower components and save to bower.json
bower install --save requirejs-plugins

Project Folders and Files

Create these folders and files. Browse, clone or download the source code if you prefer.

  • css
  • js
  • src
    • js
      • modules
        • gmap.js
        • location.js
      • require
      • main.js
    • gulpfile.js
    • index.html
    • require.build.js

Web Page

This web page contains the map container, address and geocode location inputs. jQuery is loaded separately before the require.js module loader to demonstrate how to define jQuery when it is external.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<title>Page Title</title>
	<link rel="stylesheet" href="/css/style.css">
</head>
<body>
	
	<p>Drag the marker to a location on the map and set input values.</p>
	
	<div id="map-canvas"></div>
	<label>Street
		<input type="text" id="street" name="street" />		
	</label>
	<label>Locality
		<input type="text" id="locality" name="locality" />
	</label>
	<label>Region
		<input type="text" id="region" name="region" />
	</label>
	<label>Country
		<input type="text" id="country" name="country" />
	</label>
	<label>Latitude
		<input type="text" id="latitude" name="latitude" />
	</label>
	<label>Longitude
		<input type="text" id="longitude" name="longitude" />
	</label>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
	
	<!-- Prod / built version -->
	<!--script data-main="/js/main" src="/js/require/require.js"></script-->
	<!-- Dev version -->
	<script data-main="/src/js/main" src="/src/js/require/require.js"></script>

</body>
</html>

Google Map JavaScript API

This module is dependent upon the RequireJS async plugin to load the google maps api. The gmap function accepts three parameters: the default latitude, default longitude and a callback function to handle the result data.

gmap.js
define(['async!//maps.googleapis.com/maps/api/js'],
    function(async){
    var gmap = {
        map: {},
        init: function gmap(lat, lng, callback) {
            var geocoder = new google.maps.Geocoder();
            var marker = new google.maps.Marker({
                position: new google.maps.LatLng(lat, lng),
                anchorPoint: new google.maps.Point(0, -29),
                draggable: true
            });
            map = new google.maps.Map(document.getElementById('map-canvas'), {zoom: 10});
            map.setCenter(marker.position);
            marker.setMap(map);

            google.maps.event.addListener(marker, 'dragend', function (evt) {

                var latlng = {lat: evt.latLng.lat(), lng: evt.latLng.lng()};
                var addrComponents = {
                    street_number: 'short_name',
                    route: 'long_name',
                    establishment: 'long_name',
                    locality: 'long_name',
                    administrative_area_level_1: 'short_name',
                    country: 'short_name',
                    postal_code: 'short_name'
                };
                result = {
                    address: {},
                    latitude: 0,
                    longitude: 0
                };

                geocoder.geocode({'location': latlng}, function(results, status) {
                    if (status === google.maps.GeocoderStatus.OK) {
                        if (results[0]) {

                            var i; var type = null;
                            for (i = 0; i < results[0].address_components.length; i++) {
                                type = results[0].address_components[i].types[0];
                                if (addrComponents[type]) {
                                    result.address[type] = results[0].address_components[i][addrComponents[type]];
                                }
                            }
                            result.latitude = latlng.lat;
                            result.longitude = latlng.lng;
                            if(typeof callback == "function"){
                                callback(result);
                            }

                        } else {
                            window.alert('No results found');
                        }
                    } else {
                        window.alert('Geocoder failed due to: ' + status);
                    }
                });
            });
        }
    };
    return gmap;
});

Location module

This module handles the result data from the gmap.js module and sets the input values.

location.js
define(['jquery','modules/gmap'],
    function($, gmap){
    var location = {
        // default map coordinates
        latitude: 39.084014922903,
        longitude: -77.51372591791,
        init: function(){
            gmap.init(
                location.latitude,
                location.longitude,
                function(result){
                    console.log('RESULT',result);

                    if (result.address) {
                        location._setAddress(result.address);
                    }

                    $('#latitude').val(result.latitude);
                    $('#longitude').val(result.longitude);
                }
            );
        },
        _setAddress: function(address) {
            var street = [];
            if (address.street_number) {
                street.push(address.street_number);
            }
            if (address.route) {
                street.push(address.route);
            }
            if (street.length > 0) {
                $('#street').val(street.join(' '));
            }
            if (address.locality) {
                $('#locality').val(address.locality);
            }
            if (address.administrative_area_level_1) {
                $('#region').val(address.administrative_area_level_1);
            }
            if (address.country) {
                $('#country').val(address.country);
            }
        }
    };
    location.init();
    return location;
});
main.js

JavaScript application entry point.

'use strict';

// since jQuery is already loaded from google cdn
define('jquery', [], function() {
    return jQuery;
});

require.config({
    paths : {
        //create alias to plugins (not needed if plugins are on the baseUrl)
        async: 'require/async'
    }
});

require(['modules/location'], function(location) {
     location.init();
});
gulpfile.js

This gulp file defines two tasks. 1. gulp copy copies node modules and bower components into the application. 2. gulp server runs the application in a static BrowserSync web server.

'use strict';
var gulp = require('gulp');
var browserSync = require('browser-sync').create();

// copy vendor libraries into app
gulp.task('copy', function() {
    gulp.src([
        './bower_components/requirejs-plugins/src/async.js',
        './node_modules/requirejs/require.js'
        ])
        .pipe(gulp.dest('./src/js/require/'));
});

// BrowserSync static server
gulp.task('server', function() {
    browserSync.init({
        server: {
            baseDir: './',
            directory: false,
            index: 'index.html'
        }
    });
});
require.build.js

This is the RequireJS Optimizer build configuration file. Once the entry point and modules are created, run node node_modules/requirejs/bin/r.js -o require.build.js to compile the modules into a single js/main.js file.

({
    baseUrl: './src/js',
    dir: './js',
    modules: [
        {
            name: 'main'
        }
    ],
    fileExclusionRegExp: /^(r|require.build)\.js$/,
    removeCombined: true,
    paths: {
        async: 'require/async'
    },
    preserveLicenseComments: false
})
style.css

Add this file into the css folder to declare some basic styles.

label {
	display: block;
	width: 300px;
	margin-bottom: 1em;
}
input[type='text'] {
	display: block;
	width: 100%;
}
#map-canvas {
	width: 300px;
	height: 300px;
	margin-bottom: 1em;
}

Overview

  1. Create package.json and bower.json
  2. Install node modules and bower components
  3. Create /index.html
  4. Create /src/js/modules/gmap.js
  5. Create /src/js/modules/location.js
  6. Create /src/js/main.js
  7. Create /gulpfile.js
  8. Create /require.build.js
  9. Create /css/style.css
  10. Copy the node modules and bower components into the application. In a command shell,
    run gulp copy
  11. Load the application in the web browser. In a command shell,
    run gulp server
  12. Optimize the application. In a command shell,
    run node node_modules/requirejs/bin/r.js -o require.build.js

Reference

Joomla Hello World Module

This post documents my experience while following the Creating a simple module Joomla 3.x tutorial. I am brand new to Joomla and wanted to share my experience for anyone else trying to develop, install and demo this module in their Joomla 3 environment.

After following the steps in the tutorial, you can compare your mod_helloworld files to the files I have pushed to github. Additionally, here is the file structure of the module.

Files

  • mod_helloworld
    • sql
      • mysql
        • install.mysql.utf8.sql
        • uninstall.mysql.utf8.sql
    • tmpl
      • default.php
      • index.html
    • helper.php
    • index.html
    • mod_helloworld.php
    • mod_helloworld.xml

Install the Module

I created the module directly in the Joomla modules folder. For example,
C:\xampp\htdocs\joomla3\modules\mod_helloworld\

Using XAMPP for Windows as my local Joomla development environment.

In this example, install the module as follows:

  1. Copy the mod_helloworld folder into the Joomla modules folder if it isn’t already there.
  2. In the Joomla Administration page (/administrator), navigate to Extensions > Manage
  3. Select Discover from the sub menu.
  4. Select the Discover button.
  5. Check the box next to the Hello World module.
  6. Select the Install button.
Joomla 3 Module Discovery Install
Joomla 3.x Module Discovery Install

Add the module as follows:

  1. Navigate to Extensions > Modules
  2. Select the New button
  3. Under Select a Module Type: scroll down to the Hello World! module and select it.
  4. Fill out the mod_helloworld form, enter a Title, select a language and Position.
  5. Select the Save & Close button
Joomla 3.x Hello World Module Form
Joomla 3.x Hello World Module Form

In this example, Bonjour tout le monde (Hello World in French) is displayed in position-1 at the top of the home page.

Joomla3 Home page with Hello World module
Joomla3 Home page with Hello World module