Developing a Local addon – 03
September 20, 2017
Making the code React. I'm done with React puns.
This is the third part
This post is the third one, following the first and the second, in a series where I'm chronicling my attempt at a Local addon.
The high-level idea is to be able to control the XDebug configuration of the PHP version used to serve the site allowing me to activate and deactivate XDebug and setting some relevant fields in the process; in the previous post I've exposed my understanding of Local technology stack and how to use it to make it do, through "raw" terminal commands, what I want it to do.
As any one of my projects it's a discovery process while trying to "scratch an itch"; along the way, I will make amateur mistakes in all the technological fields involved (Docker and React in this case) and I know it.
Getting it "done right" has more value for me, in this context, than "getting it done".
I've put a repository online that contains the code in its current state on the master
branch; it has more the release state of a leak than that of an alpha though.
Local add-on boilerplate
I've started working, for this add-on, from the code provided in the simple-pressmatic-addon
repository by Jeff Gould; from the same source came the Delicious Brains article that started it.
After I've cloned the plugin, removed the .git
folder to "make it mine", installed the dependencies and removed the code specific to the example from the article I'm left with the src/renderer.js
file that I've updated to add the "XDebug Control" tab:
// file src/renderer.js
'use strict'
const path = require( 'path' )
module.exports = function ( context ) {
const hooks = context.hooks
const React = context.React
const remote = context.electron.remote
const Router = context.ReactRouter
// Development Helpers
remote.getCurrentWindow().openDevTools()
window.reload = remote.getCurrentWebContents().reloadIgnoringCache
hooks.addFilter( 'siteInfoMoreMenu', function ( menu, site ) {
menu.push( {
label: 'XDebug Control',
enabled: ! this.context.router.isActive( `/site-info/${site.id}/xdebug-control` ),
click: () => {
context.events.send( 'goToRoute', `/site-info/${site.id}/xdebug-control` )
},
} )
return menu
} )
const XDebugControl = require( './Component/XDebugControl' )( context )
// Add Route
hooks.addContent( 'routesSiteInfo', () => {
return <Router.Route key="site-info-my-component" path="/site-info/:siteID/xdebug-control" component={ XDebugControl }/>
} )
}
Now comes my "PHP guy" explanation of things; a much better one is found in Delicious Brains article.
In the package.json
file I tell Local, an Electron app, that the entry point for the add-on is the lib/renderer.js
file; this seems to be a non-standard entry in the package.json
file; while I'm working on the src/renderer.js
file npm
will build and compile the distribution version to the lib
folder.
At the top of the file I initialize the context getting hold of some global variables and defining the function that will be returned bt the module I'm writing:
module.exports = function(){
// ...
}
This, quite literally, means "this module provides, exports, a function of the context".
From that context, I initialize some variables I need like React library itself, the router and the Electron remote command.
With the latter, to speed up the development process, I open Chrome dev tools to be able to debug the application; this will be removed once the application is ready to ship.
Local, sticking to a WordPress custom, allows developers to extend its functionalities using "hooks"; in this case, my add-on is adding a menu entry, the "XDebug Control" tab, to the "More" menu.
The menu entry will be enabled if the route is active and, when clicked, will try to resolve the /site-info/:siteID/xdebug-control
path; the way I do that is by hooking once again to addContent
at the end of the file.
The route itself is a new instance of the ReactRouter
component handling requests on the /site-info/:siteID/xdebug-control
path building and using an instance of the XdebugControl
component.
The XDebugControl component
The structure of each file becomes soon familiar when working with modules and the src/Components/XDebugControl.js
file makes no exception.
Digesting React flow bit by bit it's important to understand how React components work: each component [has a lifecycle] it goes through when constructed and when updated.
In the src/renderer.js
file I'm constructing an instance of the XdebugControl
class with this line:
const XDebugControl = require( './Component/XDebugControl' )( context )
This means that the following methods will be called, as soon as the Local application starts, on hte XDebugControl
component:
- constructor()
- componentWillMount()
- render()
- componentDidMount()
The tab could not be visible and the site could not be running yet all the methods above will be called.
This means none of those is a good place to do any container-related operation like trying to discern if XDebug is currently active or not.
The constructor
method proves useful to set the initial state
and the site object and, for the time being, I leave the other methods alone.
Looking again at a React component lifecycle when updating it I find out the method that will be called when the Component becomes visible is the componentWillReceiveProps
one and set up a first version of the add-on accordingly:
// file src/Components/XdebugControl.js
module.exports = function ( context ) {
const Component = context.React.Component
const React = context.React
return class XDebugControl extends Component {
constructor( props ) {
super( props )
this.state = {
machineStatus: 'off',
xdebugStatus: 'n/a',
}
this.site = this.props.sites[this.props.params.siteID]
}
componentWillMount() {}
componentWillReceiveProps( nextProps ) {
let newState = null
if ( nextProps.siteStatus === 'running' ) {
newState = {
siteStatus: 'running',
xdebugStatus: 'active',
}
} else {
newState = {
siteStatus: 'stopped',
xdebugStatus: 'n/a',
}
}
this.setState( newState )
}
componentDidMount() {}
render() {
let statusString = null
if ( this.site.environment !== 'custom' ) {
statusString = 'Only available on custom installations!'
} else {
if ( this.props.siteStatus === 'running' ) {
statusString = `Xdebug status: ${this.state.xdebugStatus}`
} else {
statusString = 'Machine non running!'
}
}
return (
<div style={{display: 'flex', flexDirection: 'column', flex: 1, padding: '0 5%'}}>
<h3>XDebug Controls</h3>
<h4><strong>{statusString}</strong></h4>
</div>
)
}
}
}
I have, in terms of rendering, a light initial state set in the constructor method and will re-render using my first dynamic parameter, the site status, when needed.
For the time being these are the two states the addon will go through:
[](https://theaveragedev.com/wp-content/uploads/2017/09/machine-not-running.png)
[](https://theaveragedev.com/wp-content/uploads/2017/09/machine-running.png)
And while this is trivial already understanding when not to perform costly operations took quite some time to me.
The key takeaway here was understanding the React component lifecycle and how it interacts with an Electron application.
Getting XDebug current status
In the current version of the code the addon is not yet interacting with the container in any way.
The first interaction I will put in place is the one to read the current XDebug status from the container; I've shown in the previous post how to do that on a bash
level interacting with the container directly but it is now time to move that logic to the add-on code.
The first modification I make is to update the componentWillReceiveNewProps
method to use a still undefined container
dependency to read the current status of XDebug. The container
property is initialized in the constructor
method passing it the docker
sub-dependency.
// file src/Components/XdebugControl.js
module.exports = function ( context ) {
const Component = context.React.Component
const React = context.React
const Docker = require( './../System/Docker' )( context )
const Container = require( './../System/Container' )( context )
return class XDebugControl extends Component {
constructor( props ) {
super( props )
this.state = {
siteStatus: 'off',
xdebugStatus: 'n/a',
}
this.site = this.props.sites[this.props.params.siteID]
this.docker = new Docker()
this.container = new Container( this.docker, this.site )
}
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps( nextProps ) {
let newState = null
if ( nextProps.siteStatus === 'running' ) {
newState = {
siteStatus: 'running',
xdebugStatus: this.container.getXdebugStatus(),
}
} else {
newState = {
siteStatus: 'stopped',
xdebugStatus: 'n/a',
}
}
this.setState( newState )
}
componentWillUnmount() {}
render() {
// ...unchanged
}
}
}
The Docker class
The Docker
class is a general-purpose wrapper around some basic Docker-related operations; I've moved its code in the src/System/Docker.js
file:
// file src/System/Docker.js
module.exports = function ( context ) {
const childProcess = require( 'child_process' )
return class Docker {
static getDockerPath() {
return context.environment.dockerPath.replace( / /g, '\\ ' )
}
static runCommand( command ) {
let dockerPath = Docker.getDockerPath()
let fullCommand = `${dockerPath} ${command}`
return childProcess.execSync( fullCommand, {env: context.environment.dockerEnv} ).toString().trim()
}
}
}
Walking the path of a the Javascript dilettante I've made both methods of the class static
, it works like PHP and it means those are class methods instead of being instance methods; the reason being that there is really no use for an instance of this class as there is no state change lacking the class any properties.
I guess speed is a factor too. I guess.
Until now I've passed the context
object around in any require
call I've made and that's to allow the classes I define to read some basic and general purpose information from it; in the case of context.environment.dockerPath
that value will be a string representing the absolute path, on the host machine, to the Docker binary bundled with Local.
On my machine that value is /Applications/Local by Flywheel.app/Contents/Resources/extraResources/virtual-machine/vendor/docker/osx/docker
.
In essence when running a command with the runCommand
method that path and the command will be passed to the node.js child process handler; from the library, I then use the synchronous execSync
method to run the command and wait for it to finish.
This is probably "PHP mentality" and I should use asynchronous processes and a cascade of callbacks to manage my application: I can only learn so much stuff in a given time.
If the command generates an exception, for any reason, I'm leaving that exception bubble up to the client object method to allow for exception-based logic to be put in place; an example of that in the Container
class below.
The Container class
Since Docker is a manager of containers it only made sense to reflect the same dependency in the class structure: the Container class will require, in its constructor
method, a docker
and a site
object; the first I've shown above and the second is, essentially, a collection of information about a single site managed by Local.
Where the Docker class made sense in static
form the Container class will make sense, having to keep track of a complex object state (not "React state", just "object state"), in instance form.
The bare-bones code of the current implementation looks like this:
// file src/System/Container.js
module.exports = function ( context ) {
const childProcess = require( 'child_process' )
const Docker = require( './Docker' )( context )
return class Container {
constructor( docker, site ) {
this.docker = docker
this.site = site
}
exec( command ) {
let fullCommand = `exec -i ${this.site.container} sh -c "${command}"`
return Docker.runCommand( fullCommand )
}
getXdebugStatus() {
try {
let status = this.exec( `wget -qO- localhost/local-phpinfo.php | grep Xdebug` )
if ( status.length !== 0 ) {
return 'active'
}
return 'inactive'
}
catch ( e ) {
return 'inactive'
}
}
}
}
All this yields the initial behavior I'm looking for, correctly displaying the lazily-loaded XDebug status of active custom installation Local sites:
[](https://theaveragedev.com/wp-content/uploads/2017/09/custom-site-xdebug-active.png)
[](https://theaveragedev.com/wp-content/uploads/2017/09/custom-machine-not-running.png)
[](https://theaveragedev.com/wp-content/uploads/2017/09/flywheel-site.png)
Next
The code I've shown in this post is on GitHub tagged post
in a not clean, commented-out-blocks-of-code form.
I'm rewriting the first minimum version in light of some more careful pacing and learned notions and that's the reason the code in the branch looks not pleasant to the eye.
I will delve into more complex code in the next post putting, or rather restore, the functionalities that make the add-on somewhat useful.