GitHub Action workflow for running PHP_CodeSniffer on pull requests with check annotations

🤔 Idea

For GitHub pull requests, detect any coding standard violations via PHP_CodeSniffer. Errors should prevent the pull request from being merged and displayed to the user.

📖 Implementation

Example of a check annotation
Example of a job output

🏗 Workflow file

YAML / RAW / github:gist
name: PHP_CodeSniffer

on: pull_request

jobs:
  phpcs:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '7.3'
          coverage: none
          tools: composer, cs2pr

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "::set-output name=dir::$(composer config cache-files-dir)"

      - name: Setup cache
        uses: pat-s/always-upload-cache@v1.1.4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          # Use the hash of composer.json as the key for your cache if you do not commit composer.lock. 
          # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-suggest --no-progress

      - name: Detect coding standard violations
        run: vendor/bin/phpcs -q --report=checkstyle | cs2pr --graceful-warnings

The workflow file can be saved in the .github/workflows directory of your GitHub repository.

Notes:

  • The workflow is using the pat-s/always-upload-cache action, a fork of actions/cache, which also supports caching if a previous step fails.
  • Your repository should include a configuration file for PHP_CodeSniffer.
  • If the coding standard isn’t one of the defaults it need to be added as (dev) dependency in your project’s composer.json file.
  • Only 10 warning and 10 error annotations per step are currently supported (source). It’s recommended to fix all existing errors before publishing the workflow.
  • To exit with error codes if there are only warnings you can remove the --graceful-warnings flag in the last line.

Questions? Feedback? Let me know in the comments!

Photo by Alexander Sinn on Unsplash

How to edit post meta outside of a Gutenberg Block?

The Block API of Gutenberg supports post meta as a attribute source. But what if you want to use post meta in say a plugin sidebar?

With the help of the @wordpress/data package and so called “higher-order components” it’s as easy as for blocks. 🙌

First we import all the required dependencies. This assumes that you’re using webpack and define your externals like this. If you’re looking for an ES5 example take a look at the block editor handbook.

/**
 * WordPress dependencies
 */
import {
	withSelect,
	withDispatch,
} from '@wordpress/data';
import {
	PluginSidebar,
	PluginSidebarMoreMenuItem,
} from '@wordpress/edit-post';
import {
	PanelColor,
} from '@wordpress/editor';
import {
	Component,
	Fragment,
	compose,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';

To render the UI we extend Component and implement the render() method. For this example I used a PanelColor component which allows a user to select a color. We’ll save the hex value in the post meta my_color_one:

/**
 * Custom component with a simple color panel.
 */
class MyPlugin extends Component {

	render() {
		// Nested object destructuring.
		const {
			meta: {
				my_color_one: colorOne,
			} = {},
			updateMeta,
		} = this.props;

		return (
			<Fragment>
				<PluginSidebarMoreMenuItem
					name="my-plugin-sidebar"
					type="sidebar"
					target="my-plugin-sidebar"
				>
					{ __( 'Color it!', 'my-plugin' ) }
				</PluginSidebarMoreMenuItem>
				<PluginSidebar
					name="my-plugin-sidebar"
					title={ __( 'Color it!', 'my-plugin' ) }
				>
					<PanelColor
						colorValue={ colorOne}
						initialOpen={ false }
						title={ __( 'Color 1', 'my-plugin' ) }
						onChange={ ( value ) => {
							// value is undefined if color is cleared.
							updateMeta( { my_color_one: value || '' } );
						} }
					/>
				</PluginSidebar>
			</Fragment>
		);
	}
}

(Please ignore the incorrect syntax highlighting.)

Now we need a higher-order component which is used to fetch the data, in this case our post meta.

// Fetch the post meta.
const applyWithSelect = withSelect( ( select ) => {
	const { getEditedPostAttribute } = select( 'core/editor' );

	return {
		meta: getEditedPostAttribute( 'meta' ),
	};
} );

The second higher-order component is used to update the data.

// Provide method to update post meta.
const applyWithDispatch = withDispatch( ( dispatch, { meta } ) => {
	const { editPost } = dispatch( 'core/editor' );

	return {
		updateMeta( newMeta ) {
			editPost( { meta: { ...meta, ...newMeta } } ); // Important: Old and new meta need to be merged in a non-mutating way!
		},
	};
} );

Since we now have two higher-order components we have to combine them with compose:

// Combine the higher-order components.
const render = compose( [
	applyWithSelect,
	applyWithDispatch
] )( MyPlugin );

Having a fully renderable component we can finally register our custom plugin:

registerPlugin( 'my-plugin', {
	icon: 'art',
	render,
} );

And that’s it! 🚢

PS: Make sure to register the post meta properly with 'show_in_rest' => true.


Photo by Kimberly Farmer.

Remove WordPress plugins under version control from update checks

Annoyed by the update information for plugins which are under version control like Git or SVN? Use this snippet to finally get rid of them.

/**
 * Removes plugins under version control from update checks.
 *
 * @param array  $request  An array of HTTP request arguments.
 * @param string $url     The request URL.
 * @return $array An array of HTTP request arguments.
 */
add_filter( 'http_request_args', function( $request, $url ) {
	if ( false === strpos( $url, 'api.wordpress.org/plugins/update-check' ) ) {
		return $request;
	}

	$vcs_dirs = [ '.git', '.svn', '.hg', '.bzr' ];

	$plugins = json_decode( $request['body']['plugins'] );
	foreach ( array_keys( (array) $plugins->plugins ) as $plugin ) {
		$plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin ) . '/';

		// Search directory for evidence of version control.
		foreach ( $vcs_dirs as $vcs_dir ) {
			if ( is_dir( $plugin_dir . $vcs_dir ) ) {
				// Remove plugin from the update check.
				unset( $plugins->plugins->$plugin );
				break;
			}
		}
	}

	$request['body']['plugins'] = wp_json_encode( $plugins );

	return $request;
}, 10, 2 );

You can extend $vcs_dirs with additional directories to add support for your VCS.

Photo by TUAN ANH TRAN.