← All articles

WordPress Development

functions.php Alternatives: Where Custom WordPress Code Should Actually Live

Child theme, site plugin, mu-plugin, or snippet manager? A practical decision guide with working examples — plus how to share state between snippets without haunted globals.

Updated 5 min read Mark Ashton

  • wordpress
  • workflow
  • php
Abstract cover artwork for functions.php Alternatives: Where Custom WordPress Code Should Actually Live

"Add this to your functions.php" is the default answer on every WordPress forum because it always works the first time. The trouble starts on the fifth snippet, the first theme switch, or the first syntax error on a live store.

There are four real alternatives. Each one is correct for a specific job. This guide shows what each looks like, how it fails, and which one to pick.

The short answer

  • Code that styles or depends on one theme → child theme functions.php
  • Code that must run no matter what and never be switched off → mu-plugin
  • A feature with its own settings, assets, or tests → a site-specific plugin
  • Everything else — hooks, filters, tweaks, tracking, WooCommerce rules → a snippet manager

Most sites have mostly "everything else." That is why snippet managers exist.

1. Child theme functions.php

A child theme stops parent-theme updates from wiping your edits. That is the only problem it solves.

  • Fails when: you switch themes (every hook goes with it), or a syntax error white-screens the front end *and* wp-admin, because the theme loads on both.
  • Good for: add_theme_support, image sizes, and template tweaks that only make sense with this theme.

If a hook would still be needed after a redesign — a checkout rule, a redirect, an analytics ID — it does not belong in the theme.

2. A site-specific plugin

A single-file plugin is ten lines of header and your code. WordPress loads it independently of the theme and you can deactivate it from the Plugins screen.

<?php
/**
 * Plugin Name: Acme Site Customizations
 * Description: Site-specific hooks for acme.com. Not for distribution.
 * Version: 1.0.0
 * Requires PHP: 8.0
 */

defined( 'ABSPATH' ) || exit;

add_filter( 'woocommerce_checkout_fields', function ( $fields ) {
	unset( $fields['billing']['billing_company'] );
	return $fields;
} );
  • Fails when: it grows into a 900-line file of unrelated hooks. You can only turn the whole thing on or off, and a fatal in any hook takes down every other hook with it.
  • Good for: a real feature — a custom post type with its own admin screen, assets, and tests — that deserves a repo and a version number.

3. Must-use plugins

Files in wp-content/mu-plugins/ load on every request, before normal plugins, and cannot be deactivated from the admin. WordPress only loads top-level files there, so a small loader is common:

<?php
// wp-content/mu-plugins/acme-loader.php
foreach ( glob( __DIR__ . '/acme/*.php' ) as $file ) {
	require_once $file;
}
  • Fails when: something breaks and the only off switch is SFTP. "Cannot be deactivated" is a feature right up until it is the problem.
  • Good for: infrastructure that must never be toggled by a client — security headers, environment flags, object-cache config, forcing HTTPS behind a proxy.

4. A snippet manager

A snippet manager gives every hook its own on/off switch, type (PHP, CSS, JS, HTML), and scope. The important differences between tools are not the editor — they are storage and recovery.

  • Database-stored snippets (WPCode, the free Code Snippets plugin) run PHP out of a table. Backups are easy; recovery when PHP is fatal is not, because the code that crashes is loaded by the same request that would render the admin.
  • File-based snippets (FluentSnippets, SnipVault) store each snippet as a real file. You can grep them, back them up with wp-content, and disable one over SFTP by renaming it.

What you should expect from any of them in 2026:

  • Per-snippet activation, so one broken hook does not take the rest with it
  • Fatal-error handling that deactivates the offending snippet instead of the site
  • A recovery path that works when the front end is down — SnipVault never runs snippets on its own admin screen, so /wp-admin/options-general.php?page=snipvault stays reachable
  • Conditional loading so checkout code does not run on the blog

Past that, the professional features are about workflow: GitHub sync, a security audit before publish, and managing the same snippet across many sites.

Sharing state between snippets

The moment code is split into separate snippets, someone needs to pass a value from one to another. The first thing that looks like it works is a PHP global. It does work — and it silently collides with every other snippet that picked the same name.

Every snippet runs in the same PHP request. Two snippets that both write global $enabled; share that variable whether their authors meant to or not. Rules that hold up:

  • Prefix everything with a project slug: acme_vat_rate, never rate, data, config, or settings.
  • Never overwrite core globals such as $post, $wp_query, or $wpdb. Read them; do not reassign them.
  • Use constants for true constants. A value that never changes at runtime is a define(), not a global. SnipVault's PHP Global Variables settings screen defines these once for every snippet.
  • Put mutable state behind one accessor so the name appears in exactly one place:
<?php
function acme_get_vat_rate(): float {
	static $rate = null;

	if ( null === $rate ) {
		$rate = (float) apply_filters( 'acme_vat_rate', get_option( 'acme_vat_rate', 0.2 ) );
	}

	return $rate;
}

Every other snippet calls acme_get_vat_rate(). You get a single grep target, a filter for tests, and a place to add logging when the number is wrong in production. If the value must survive the request, it belongs in an option, a transient, or the object cache — not a global.

Migrating out of functions.php

  1. Inventory. Open the child theme's functions.php and list every hook, one line each. Mark the ones that are genuinely theme-specific.
  2. Split by job. One snippet per job — woo-hide-company-field, not misc-fixes.
  3. Stage first. Move a snippet, deactivate the original block with a comment, test the pages that hook touches.
  4. Watch for load order. Theme functions.php runs after plugins load. A snippet that calls a theme function directly needs to wait for after_setup_theme or init.
  5. Delete, do not comment out, once verified. Commented code in functions.php is how a later "cleanup" re-enables it.

Which should you pick?

For a single personal site with three tweaks, a child theme is fine. For client work, stores, or any site you will maintain for years, give every hook its own switch from day one. Migrating fifty scattered functions.php blocks across twenty sites later costs far more than starting in the right place.

Next

Ready to upgrade your snippet workflow?

GitHub sync, the Snippet Engineer, and security auditing — in one WordPress plugin.