Categories
Programming

Strongly Typed WP-API

The first in a series of posts exploring WP-API with statically typed PHP and Functional Programming patterns.

The Context

To expose a resource as an endpoint via WordPress’ WP-API interface one must use register_rest_route.

/** * Registers a REST API route. * * Note: Do not use before the {@see 'rest_api_init'} hook. * * @since 4.4.0 * @since 5.1.0 Added a _doing_it_wrong() notice when not called on or after the rest_api_init hook. * * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin. * @param string $route The base URL for route you are adding. * @param array $args Optional. Either an array of options for the endpoint, or an array of arrays for * multiple methods. Default empty array. * @param bool $override Optional. If the route already exists, should we override it? True overrides, * false merges (with newer overriding if duplicate keys exist). Default false. * @return bool True on success, false on error. */ function register_rest_route( $namespace, $route, $args = array(), $override = false ) {

The documentation here is incredibly opaque so it’s probably a good idea to have the handbook page open until the API is internalized in your brain.

The $namespace and $route arguments are somewhat clear, however in typical WordPress PHP fashion the bulk of the magic is provided through an opaquely documented @param array $args.

The bare minimum are the keys method and callback and for our purposes will be all that we need. WP_REST_Server provides some handy constants (READABLE, CREATABLE, DELETABLE, EDITABLE) for the methods key so that leaves callback.

What is callback? In PHP terms it’s a callable. Many things in PHP can be a callable. The most commonly used callable for WordPress tends to be a string value that is the name of a function:

function my_callable() { } register_rest_route( 'some-namespace', '/some/path', [ 'callback' => 'my_callable' ] );

This would call my_callable, and as is would probably return 200 response with an empty body.

What would me more useful than just callable would be a callable that can define its argument types and return types.

Types and PHP

The ability to verify the correctness of software with strongly typed languages is an obvious benefit to using them.

However, an additional benefit is how the types themselves become the natural documentation to the code.

PHP has supported type hinting for a while:

function totes_not_buggy( WP_REST_Request $request ) WP_REST_Response { }

With type hints the expectations for totes_not_buggy() are much clearer.

Adding these type hints means at runtime PHP will enforce that only instances of WP_REST_Request will be able to be used with totes_not_buggy(), and that totes_not_buggy() can only return instances of WP_REST_Response.

This sounds good except that this is enforced at runtime. For true type safety we want something better, we want static type analysis. Types should be enforced without running the code.

For this exercise, Psalm will provide static type analysis via PHPDoc annotations.

/** * Responds to a REST request with text/plain "You did it!" * * @param WP_REST_Request $request * @return WP_REST_Response */ function totes_not_buggy($request) { return new WP_REST_Response( 'You did it!', 200, ['content-type' => 'text/plain' ); }

Ok this all sounds nice in theory, how do we check this with Psalm?

To the terminal!

mkdir -p ~/code/wp-api-fun cd ~/cod/wp-api-fun composer init

Accept all the defaults and say “no” to the dependencies:

Package name (<vendor>/<name>) [beaucollins/wp-api-fun]: Description []: Author [Beau Collins <beau@collins.pub>, n to skip]: Minimum Stability []: Package Type (e.g. library, project, metapackage, composer-plugin) []: License []: Define your dependencies. Would you like to define your dependencies (require) interactively [yes]? no Would you like to define your dev dependencies (require-dev) interactively [yes]? no { "name": "beaucollins/wp-api-fun", "authors": [ { "name": "Beau Collins", "email": "beau@collins.pub" } ], "require": {} } Do you confirm generation [yes]?

Now install two dependencies:

  • vimeo/psalm to run type checking
  • php-stubs/wordpress-stubs to type check against WordPress APIs
composer require --dev vimeo/psalm php-stubs/wordpress-stubs

Assuming success try to run Psalm:

./vendor/bin/psalm Could not locate a config XML file in path /Users/beau/code/wp-api-fun/. Have you run 'psalm --init' ?

To keep things simple with composer, define a single PHP file to be loaded for our project at the path ./src/fun.php:

mkdir src touch src/fun.php

Now inform composer.json where this file is via the "autoload" key:

{ "name": "beaucollins/wp-api-fun", "authors": [ { "name": "Beau Collins", "email": "beau@collins.pub" } ], "require": {}, "require-dev": { "vimeo/psalm": "^3.9", "php-stubs/wordpress-stubs": "^5.3" }, "autoload": { "files": ["src/fun.php"] } }

Generate Psalm’s config file and run it to verify our empty PHP file has zero errors:

./vendor/bin/psalm --init Calculating best config level based on project files Calculating best config level based on project files Scanning files... Analyzing files... ░ Detected level 1 as a suitable initial default Config file created successfully. Please re-run psalm.
./vendor/bin/psalm Scanning files... Analyzing files... ░ ------------------------------ No errors found! ------------------------------ Checks took 0.12 seconds and used 37.515MB of memory Psalm was unable to infer types in the codebase

For a quick gut-check define totes_not_buggy() in ./src/fun.php:

<?php // in ./src/fun.php /** * Responds to a REST request with text/plain "You did it!" * * @param WP_REST_Request $request * @return WP_REST_Response */ function totes_not_buggy($request) { return new WP_REST_Response( 'You did it!', 200, ['content-type' => 'text/plain' ); }

Now analyze with Psalm:

./vendor/bin/psalm ./vendor/bin/psalm Scanning files... Analyzing files... E ERROR: UndefinedDocblockClass - src/fun.php:6:11 - Docblock-defined class or interface WP_REST_Request does not exist * @param WP_REST_Request $request ERROR: UndefinedDocblockClass - src/fun.php:7:12 - Docblock-defined class or interface WP_REST_Response does not exist * @return WP_REST_Response ERROR: MixedInferredReturnType - src/fun.php:7:12 - Could not verify return type 'WP_REST_Response' for totes_not_buggy * @return WP_REST_Response ------------------------------ 3 errors found ------------------------------ Checks took 0.15 seconds and used 40.758MB of memory Psalm was unable to infer types in the codebase

Psalm doesn’t know about WordPress APIs yet. Time to teach it where those are by adding the stubs to ./psalm.xml:

<stubs> <file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php" /> </stubs> </psalm>

One more run of Psalm:

./vendor/bin/psalm Scanning files... Analyzing files... ░ ------------------------------ No errors found! ------------------------------ Checks took 5.10 seconds and used 356.681MB of memory Psalm was able to infer types for 100% of the codebase

No errors! It knows about WP_REST_Request and WP_REST_Response now.

What happens if they’re used incorrectly like a string for the status code in the WP_REST_Response constructor:

ERROR: InvalidScalarArgument - src/fun.php:10:48 - Argument 2 of WP_REST_Response::__construct expects int, string(200) provided return new WP_REST_Response( 'You did it!', '200', ['content-type' => 'text/plain'] );

Nice! Before running the PHP source, Psalm can tell us if it is correct or not. IDE’s that have Psalm integrations show the errors in-place:

Screen capture of Visual Studio Code with fun.php open and the Psalm error displayed in a tool tip.
Visual Studio Code with the Psalm extension enabled showing the InvalidScalarArgument error. ]

Now to answer the question “which type of callable is the register_rest_route() callback option?”

First-Class Functions

With PHP’s type hinting, the best type it can offer for the callback parameter is callable.

This gives no insight into which arguments the callable requires nor what it returns.

With Psalm integrated into the project there are more tools available to better describe this callable type.

callable(Type1, OptionalType2=, SpreadType3...):ReturnType

Using this syntax, the callback option of $args can be described as:

callable(WP_REST_Request):(WP_REST_Response|WP_Error|JSONSerializable)

This line defines a callable that accepts a WP_REST_Request and can return one of WP_REST_Response, WP_Error or JSONSerializable.

Once returned, WP_REST_Server will do what is required to correctly deliver an HTTP response. Anything that conforms to this can be a callback for WP-API. The WP-API world is now more clearly defined:

callable(WP_REST_Request):(WP_REST_Response|WP_Error|JSON_Serializable)

To illustrate this type at work define a function that accepts a callable that will be used with register_rest_route().

Following WordPress conventions, each function name will be prefixed with totes_ as an ad-hoc namespace of sorts (yes, this is completely ignoring PHP namespaces).

/** * @param string $path * @param (callable(WP_REST_Request):(WP_REST_Response|WP_Error|JSONSerializable)) $handler * @return void */ function totes_register_api_endpoint( $path, $handler ) { register_rest_route( 'totes', $path, [ 'callback' => $handler ] ); } add_action( 'rest_api_init', function() { totes_register_api_endpoint('not-buggy', 'totes_not_buggy'); } );

A quick check with Psalm shows no errors:

------------------------------ No errors found! ------------------------------

What happens if the developer has a typo in the string name of the callback totes_not_buggy? Perhaps they accidentally typed totes_not_bugy?

ERROR: UndefinedFunction - src/fun.php:24:45 - Function totes_not_bugy does not exist totes_register_api_endpoint('not-buggy', 'totes_not_bugy');

Fantastic!

What happens if the totes_not_buggy function does not conform to the callable(WP_REST_Request):(...) type? Perhaps it returns an int instead:

/** * Responds to a REST request with text/plain "You did it!" * * @param WP_REST_Request $request * @return int */ function totes_not_buggy( $request ) { return new WP_REST_Response("not buggy", 200, ['content-type' => 'text/plain']); }
ERROR: InvalidArgument - src/fun.php:24:45 - Argument 2 of totes_register_api_endpoint expects callable(WP_REST_Request):(JSONSerializable|WP_Error|WP_REST_Response), string(totes_not_buggy) provided totes_register_api_endpoint('not-buggy', 'totes_not_buggy');

The callable string 'totes' no longer conforms to the API. Psalm is catching these bugs before anything is even executed.

But Does it Work?

Psalm says this code is correct, but does this code work? Well, there’s only one way to find out.

First, turn./src/fun.php into a WordPress plugin with the minimal amount of header comments:

<?php /** * Plugin Name: Totes */

And boot WordPress via wp-env:

npm install -g @wordpress/env echo '{"plugins": ["./src/fun.php"]}' > .wp-env.json wp-env start curl http://localhost:8889/?rest_route=/ | jq '.routes|keys' | grep totes

There are the endpoints:

curl --silent http://localhost:8889/\?rest_route\=/ | \ jq '.routes|keys' | \ grep totes "/totes", "/totes/not-buggy",
curl http://localhost:8889/\?rest_route\=/totes/not-buggy "not buggy"

Well it works, but there’s a small problem. It looks like WordPress decided to json_encode() the string literal not buggy so it arrived in quotes as "not buggy" (not very not buggy).

Changing the return of totes_not_buggy to something more JSON compatible works as expected:

- return new WP_REST_Response("not buggy", 200, ['content-type' => 'text/plain']); + return new WP_REST_Response( [ 'status' => 'not-buggy' ] );
curl http://localhost:8889/\?rest_route\=/totes/not-buggy {"status":"not-buggy"}

Automate It

Reproducing the steps to run psalm on this codebase is trivial.

With a concise Github Action definition this project can get static analysis on every push. Throw in a annotation service and Pull Request changes are marked with Psalm warnings and exceptions.

Screenshot of an annotated Pull Request on GitHub.

The Github workflow definition defines how to:

  1. Install composer.
  2. Install composer dependencies (with caching).
  3. Run composer check.
  4. Report the Psalm errors.

The Fun Part

This sets up the foundation for a highly productive development environment:

  • Psalm static analysis provides instant feedback on correctness of code.
  • wp-envallows for fast verification of running code.
  • GitHub Actions automates type checking as an ongoing concern.

Coming up: exploring functional programming patterns for WP-API with the help of Psalm.

#ci, #php, #progamming

Categories
Programming

Learning to Like Exceptions

If you would have told me two years ago that I would being writing Java for my livelihood I would have punched you.

Transitioning from more dynamically typed environments to Java felt like I was being bossed around by javac and I hated it. The most tedious example of this was exception handling. A few projects and library later I’ve learned to love them.

Your Methods Lie

Look at any Android project’s source code you’re going to see source code riddled with null checks like this:

Object thing = mWidget.getThing();

if (thing != null) {
  thing.doSomething();
}

The problem is mWidget.getThing() lied to us. It says it returns Object but it in fact can return nothing or in Java: null1.

Usually the null check exists because at some point calling thing.doSomething() caused a NullPointerException and some poor user experienced a crashing app.

In Java you can’t completely avoid null checks but you can do things to avoid exacerbating the problem.

Don’t be Afraid to Throw

In my efforts to make things easier I find that many times I create a utility method that wraps another “exception happy” method (like working with io).

public class UploadUtil {

  public static Uri uploadImage(Bitmap image) {
    try {
        String filename = BitmapUtil.generateUniqueJpgFilename(filenamePrefix);
        File file = BitmapUtil.saveBitmapAsJpg(photo, Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), filename);

        return Uri.fromFile(file);
    } catch (IOException exception) {
      return null;
    }
  }

}

Pretty simple to follow: provide a Bitmap get back a Uri and do something with the Uri:

Uri uri = UploadUtil.uploadImage(myBitmap);

Request request = new StreamFileUploadRequest(uri);

But now whenever I use the UploadUtil.uploadImage(Bitmap) method I have to remember to do a null check every time.

If another developer (e.g. my future self) were to use this method they probably won’t know to do a null check unless:

  1. They have access to the source code and read it
  2. The null case is documented
  3. They read the documentation

I thought I was saving myself some effort by handling the exception in one place, but it’s now even worse because instead of a compile time error this has a high chance of not being discovered until a NullPointerException crash.

Nothing is Wrong

The method signature of UploadUtil.uploadImage(Bitmap) says it returns a Uri, so let’s make sure we’re not lying anymore by also returning nothing:

public class UploadUtil {

  public static Uri uploadImage(Bitmap image)
  throws IOException {
    String filename = BitmapUtil.generateUniqueJpgFilename(filenamePrefix);
    File file = BitmapUtil.saveBitmapAsJpg(photo, Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), filename);

    return Uri.fromFile(file);
  }

}

That’s easy, just throw the exception. Now the user of uploadImage(Bitmap) will have an explicit code path for handling this:

try {
  Uri uri = UploadUtil.uploadImage(myBitmap);
  Request request = new StreamFileUploadRequest(uri);
} catch (IOException exception) {
  Log.e(TAG, "Failed to upload", e);
  notifyUser(exception);
}

Another benefit is the compiler will now point out all the places you should have been checking for null.

Be Nice to Others

I used to hate using methods that threw exceptions because of the try/catch dance but I have learned that a thrown Exception is another developer looking out for me.

I have some general guidelines for myself now concerning exceptions:

  1. Instead of returning null throw an Exception if it makes sense.
  2. Wait to catch exceptions at the highest level possible. Ideally when the software is directly interacting with the user.

Exceptions can feel heavy handed and don’t make sense in every case so I’m not dogmatic about these guidelines. If you’re doing Android development using Android Studio then take a look at Nullable annotations.


  1. One exciting thing about Swift is that it solves this problem with optionals.↪︎

#exceptions, #java

Categories
Programming Tools

Android Debugging with JDB and TextMate

For android development I do my best to avoid Eclipse by using TextMate and the command line. The biggest missing piece with this setup was an easy way to get a debugger up and running. A quick trip to Google landed me on Command Line Android Development: Debugging which outlines how to get jdb attached to a running Android app instance.

I quickly grew tired of typing all of the breakpoints out and invoking a handful of commands, so I hacked together a TextMate bundle I named Android Debug to automate the process.

I have never found a use for TextMate’s bookmarking feature so it seemed like a great place to identify breakpoints. When you invoke the debugging command from TextMate it will find all of the bookmarked lines in the *.java files in your src folder and dump them into a .jdbrc file.

4_26_13_6_22_PM

To figure out which app to launch I parse the AndroidManifest.xml file for the package id and main Activity then launch the app in Waiting For Debugger mode.

4_26_13_6_27_PM

Once the app is up and waiting a jdb instance is launched and reads the breakpoints in from the .jdbrc file. After getting familiar with all the jdb commands I feel pretty comfortable debugging this way.

4_26_13_6_29_PM

Now that I finally have a quick way to debug I can go easy on adb logcat. I’m going to try to automate more parts of my Android development workflow in this bundle. There’s probably some good stuff to steal from the abandoned Android TextMate Bundle.

The only remaining pain point for me in this whole setup is ant. I’d really love it if someone could show me how to get ant debug to compile faster. Currently changing a single *.java file requires 30 seconds to get an apk compiled. It looks like dx is taking a long time to merge all of the pre-dexed libraries the WordPress for Android project is using.

#android, #debugging, #jdb, #textmate

Categories
Programming

Are you responsive?

My work has had me focused on making websites more responsive. Part of taking a non-responsive design and back-porting some media queries into the CSS is identifying where the breakpoints for a particular design exist.

To aid in identifying where these breakpoints are I built a page with an iframe in it that would tell me how wide it is at any given time. More features were added and eventually we had a useful little tool. So here it is for your pleasure, the elegantly named:

HTTP://ICANHAZ.RESPONSIV.ES/

One particularly useful features is the bookmarklet. Drag that thing to your browser’s bookmark bar and then click it when you want to load up whatever page you happen to be looking at.

If you’d like to check out the source code it’s on Github. It’s mostly client side Javascript but with a little Node.js and CoffeeScript to help determine the X-FRAME-OPTIONS header for the site you are loading.

#coffeescript, #javascript, #node-js, #tools

Categories
Programming Software

Source Control

If a piece of software claims to be able to manage my source code, I want it to manage all of my source code. Let me describe a tool that I use daily for my job whose purpose is to manage source code.

The Tool

This tool is quite simple to use. It’s been around for a while now. It has a straight forward interface and a very easy model of use to understand. I checkout code. I update code. I change code. I update code. I commit code. Pretty dead easy. Maybe a conflict happens but not really usually a big deal.

Collaborating

Sometimes I need to share some of these changes with a collaborator so I use The Tool to make a patch file. I then email/upload/transfer it in some way to my collaborator who — if her code is in the same working state and she knows how to use a tool to apply my patch file to her working copy — she can then apply the changes that are represented in my patch file. Meanwhile, if I make any changes to my working copy my patch may no longer even apply to my own working copy. At this point it could be useful to note what revision my working copy was at when I made the patch, you know, just for sanity’s sake.

Let me reiterate what just happened there. If I want to share some changes that I have made to my source code, I have to use tools other than The Tool (the one responsible for source control). Does that strike anyone else as a little odd? I need to use a tool other than my source control tool to manage my source code.

The patch file that I make has no context attached to it. It knows not which repository it came from nor the state of the repository when it was created. Very quickly the repository is going to change because there are fifty (maybe even more people) committing to this repository all the time.

Experimenting

Sometimes I get an idea — or even less — an inkling of an idea. I want to test it out in my own little sandbox and experiment with it and see if it can go anywhere. This idea consists mostly of new files but it also requires me to modify some existing ones. Time has passed and the idea is somewhat working but I need to get to something else more pressing. Ok, so what do I do with this experiment? I certainly don’t want to lose it because even though it’s not fully baked, there is some value in it.

Guess what? You’re screwed. You can create a patch and save it, but inevitably the files you modified will be changed. Maybe you make a branch1, but no, branches are for important things not your little experiments. Imagine how messy the branches folder would be if everyone used it to dump their little experiments.

Identifying the Problem

I started explaining my woes to one of my coworkers. I try my best to be diplomatic because I am not one to get involved with flame wars. The problem was identified and alas the problem is not The Tool. Apparently it’s my workflow. So perhaps I’m The Tool.

Did I mention how I love Git?

  1. Yes, the “b” word. Please excuse my language.

#git, #software-2, #tools, #workflow

Categories
Programming

Explore the WordPress.com REST API

WordPress.com has unveiled a new REST API and I wrote a tool to help debug and explore it.

In fact, the documentation for the REST API is built by the API itself! With this information we were able to build a console to help debug and explore the various resources that are now available through the new API. So let me introduce you to the new REST console for WordPress.com.

#elsewhere, #rest

Categories
Programming

I write code

I have never labeled myself a programmer. As one who has learned the craft via the omnipotent Google search box and the sharing of open source codes, I have always felt that I have yet to venture through the initiation rite that I am told consists of reading The Dragon Book and K&R, and building a compiler. Or maybe it’s because I prefer the dynamic, loosely-typed, “toy”, scripting languages to the ones real programmers use.

Regardless of how I am identified I do have a ferocious appetite to learn new things, try new tools, and challenge anything that becomes a little too precious. So in order to prevent myself from stagnating I have intentionally not self-identified what it is I do for a living other than “write code”. I write a fair amount of PHP but I am not a PHP Developer1. I have recently spent most of my time writing Javascript in the DOM but I do not identify as a Javascript Developer2. I spent a solid 2-3 years writing Ruby for eight hours a day learning how to “meta-program” as well as craft a gem and do “test driven development” but I would not consider myself a Ruby Developer3.

All of these different languages brought with them different ways of doing things and, more importantly, different people and cultures for me to learn from.

To put it as plainly as possible: I enjoy solving problems using computers. My solutions tend to involve a web browser and a web server. When this no longer interests me or — more depressingly — I can no longer maintain the skills necessary to make a living doing it, I will stop.

Until then I write code.

  1. Sometimes I want to kick PHP right between the “H”
  2. Sorry, I mean “Javascript Ninja”
  3. Sorry, I mean “Ruby Rockstar”

#about, #code