[{"data":1,"prerenderedAt":39},["ShallowReactive",2],{"blog-post-supporting-a-plugin-across-wordpress-versions":3},{"slug":4,"title":5,"description":6,"date":7,"updated":8,"tags":9,"pillar":15,"draft":16,"image":17,"imageAlt":18,"html":19,"markdown":20,"faq":21,"readingTimeText":37,"readingTimeMinutes":38},"supporting-a-plugin-across-wordpress-versions","Supporting a plugin across WordPress versions I can't control","A compatibility strategy for WordPress and PHP versions you don't control: function_exists and capability checks, graceful degradation, a support matrix.","2026-04-22T00:00:00.000Z",null,[10,11,12,13,14],"wordpress","php","compatibility","plugins","support","plugin-modernization",false,"/blog-covers/plugin-modernization.png","Plugin modernization cover","\u003Cdiv class=\"post-tldr\">\u003Cdiv class=\"post-tldr-label\">TL;DR\u003C/div>\u003Cp>You cannot control the WordPress and PHP versions your plugin lands on. Detect capabilities at runtime with function_exists and current_user_can, degrade gracefully when something is missing, and publish a support matrix with a hard floor enforced at activation.\u003C/p>\u003C/div>\n\u003Cp>The hardest constraint in plugin work is that you ship one codebase to environments you do not control and cannot see. My affiliate, licensing, and link plugins run on WordPress installs from current down to versions years old, on PHP from modern down to whatever the cheap host froze on, next to a thousand other plugins that also monkey with the same hooks. I cannot pin any of it. That same lack of control is why I \u003Ca href=\"/blog/you-dont-rewrite-a-plugin-with-a-big-install-base\">never rewrite one of these plugins in a single big push\u003C/a>, because the install base is too varied to bet on. So the strategy is not “require the latest,” it is: detect what exists at runtime, degrade gracefully when it does not, and publish a support matrix so both I and the user know where the line is.\u003C/p>\n\u003Cp>Here is how each of those three pieces works in practice.\u003C/p>\n\u003Ch2 id=\"detect-capabilities-at-runtime%2C-do-not-assume-versions\" tabindex=\"-1\">Detect capabilities at runtime, do not assume versions\u003C/h2>\n\u003Cp>Checking \u003Ccode>$wp_version\u003C/code> to decide whether a feature is available is the wrong instinct. The version string tells you what someone claims to run, not what functions and classes are actually loaded, and plenty of setups backport or strip things. The reliable question is not “which version is this” but “does the thing I need exist right now.” So I ask directly.\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">function\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> affapp_can_use_block_bindings\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    // Introduced in WP 6.5. Ask for the function, not the version.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> function_exists\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'register_block_bindings_source'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">affapp_can_use_block_bindings\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    register_block_bindings_source\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp/referral-url'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">array\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        'label'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">              =>\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> __\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Referral URL'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        'get_value_callback'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'affapp_referral_binding_value'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ) );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The same discipline covers PHP itself. Before I use a language feature or standard-library function that is not universally present, I check for it rather than assume the interpreter has it.\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// str_contains is PHP 8+. Some hosts are still on 7.4.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> function_exists\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'str_contains'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    function\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> str_contains\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( $haystack, $needle ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $needle \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">||\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> !==\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> strpos\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( $haystack, $needle );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Ccode>function_exists\u003C/code>, \u003Ccode>class_exists\u003C/code>, and \u003Ccode>method_exists\u003C/code> are how I write code that is honest about its environment. It asks the room what is present and acts on the answer, instead of trusting a version number that may be lying or backported.\u003C/p>\n\u003Ch2 id=\"capability-checks%2C-not-role-name-checks\" tabindex=\"-1\">Capability checks, not role name checks\u003C/h2>\n\u003Cp>The same “ask, do not assume” rule applies to permissions. WordPress lets site owners, membership plugins, and other code remap what roles can do. Checking whether the current user is literally an \u003Ccode>administrator\u003C/code> is fragile, because a shop might grant an “affiliate manager” role the exact capabilities without giving it that role name. So I check the capability, and I register my own where a built-in one does not fit.\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// Wrong: assumes the role name and the capability are the same thing.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">in_array\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'administrator'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">wp_get_current_user\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">roles, \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) ) { }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// Right: ask whether this user can do this specific thing.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">current_user_can\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'manage_affiliates'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    affapp_render_admin_panel\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>I add \u003Ccode>manage_affiliates\u003C/code> to the roles I ship for on activation, and site owners can grant it wherever they like. The plugin never asks “who are you,” it asks “are you allowed to do this,” which is the only question that survives contact with a real site’s permission setup.\u003C/p>\n\u003Ch2 id=\"graceful-degradation-over-hard-requirements\" tabindex=\"-1\">Graceful degradation over hard requirements\u003C/h2>\n\u003Cp>When a capability is missing, the plugin should do less, not die. A hard \u003Ccode>wp_die()\u003C/code> on an old PHP version turns one unsupported host into a support ticket and a one-star review. Degrading means the core feature still works and the enhancement is simply absent, ideally with a quiet notice so the site owner knows why.\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add_action\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'admin_init'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">function\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">version_compare\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">PHP_VERSION\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'7.4'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'&#x3C;'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">        // Do not fatal. Disable the feature that needs it, tell the owner.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        add_option\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp_advanced_reports'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        add_action\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'admin_notices'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp_php_notice'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">} );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">function\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> affapp_php_notice\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    echo\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '&#x3C;div class=\"notice notice-warning\">&#x3C;p>'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    echo\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc_html__\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        'Advanced reports need PHP 7.4 or newer. The rest of the plugin works normally.'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        'affapp'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    echo\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '&#x3C;/p>&#x3C;/div>'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The reason this matters more on a plugin than an app is that the user did not choose their PHP version for you. They are on a host, the host set the version, and moving hosts is a project. My job is to keep the core commission tracking working on their old box while the shiny report is politely unavailable, not to hold their whole plugin hostage to a version bump they cannot make this week. This is the same instinct I bring to \u003Ca href=\"/blog/shipping-a-feature-into-a-five-year-old-plugin\">shipping a feature into a five-year-old plugin\u003C/a>, where the new capability arrives behind a check and the old path keeps working untouched.\u003C/p>\n\u003Cp>There is a floor, though. Below some version I will not test against, degrading turns into pretending. That is what the matrix is for.\u003C/p>\n\u003Ch2 id=\"publish-a-support-matrix-so-the-line-is-explicit\" tabindex=\"-1\">Publish a support matrix so the line is explicit\u003C/h2>\n\u003Cp>A support matrix is a written statement of which combinations I test and stand behind, which I tolerate, and which I refuse. It protects the user from silent breakage and protects me from being on the hook for a PHP 5.6 box I have not run in years. I keep it in the readme and in my head, and I enforce the hard floor in code.\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>WordPress\u003C/th>\n\u003Cth>PHP\u003C/th>\n\u003Cth>Status\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Current and one back\u003C/td>\n\u003Ctd>8.1+\u003C/td>\n\u003Ctd>Fully tested, all features\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Two to four back\u003C/td>\n\u003Ctd>7.4 to 8.0\u003C/td>\n\u003Ctd>Supported, advanced features may degrade\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Older than that\u003C/td>\n\u003Ctd>7.4+\u003C/td>\n\u003Ctd>Best effort, core only, no guarantees\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Any\u003C/td>\n\u003Ctd>Below 7.4\u003C/td>\n\u003Ctd>Not supported, activation blocked\u003C/td>\n\u003C/tr>\n\u003C/tbody>\n\u003C/table>\n\u003Cp>The bottom row is enforced, not aspirational. I check it in the main plugin file before anything else loads, and I fail the activation cleanly rather than half-loading into a fatal.\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">register_activation_hook\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">__FILE__\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">function\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">version_compare\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">PHP_VERSION\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'7.4'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'&#x3C;'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        deactivate_plugins\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">plugin_basename\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">__FILE__\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ) );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        wp_die\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">            esc_html__\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'This plugin requires PHP 7.4 or newer.'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">            esc_html__\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Plugin activation error'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'affapp'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">            array\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">( \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'back_link'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">} );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Blocking activation on the hard floor is kinder than letting it install and fatal later, because the user gets a clear message at the moment they act, with a back link, instead of a white screen after the fact.\u003C/p>\n\u003Cp>The matrix is not static. When a new WordPress or PHP release changes behavior I depend on, I update the matrix before I touch the code, adjust the runtime checks, and, when the change reaches stored data, roll it out the same careful way I handle \u003Ca href=\"/blog/backward-compatible-migrations-on-an-install-base\">backward-compatible migrations on an install base\u003C/a>.\u003C/p>\n\u003Ch2 id=\"the-strategy-in-one-line\" tabindex=\"-1\">The strategy in one line\u003C/h2>\n\u003Cp>Ask the environment what it can do, do less when it can do less, and write down where you stop. \u003Ccode>function_exists\u003C/code> and \u003Ccode>current_user_can\u003C/code> over version and role assumptions. A notice and a disabled feature over a fatal. A published matrix with an enforced floor over silent hope. I cannot control the WordPress and PHP versions my plugins land on, and I stopped trying. The whole compatibility strategy is accepting that I do not control the room, and writing code that behaves well no matter which room it wakes up in.\u003C/p>\n\u003Cdiv class=\"post-takeaway\">\u003Cdiv class=\"post-takeaway-label\">The takeaway\u003C/div>\u003Cp>Compatibility across versions you cannot see comes down to honesty about the environment. Ask what functions, classes, and capabilities are actually present, do less when the answer is less, and block activation cleanly below the floor you are willing to test. A runtime check beats a version assumption, a disabled feature with a notice beats a white screen, and a published matrix with an enforced floor beats silent hope on every host your plugin wakes up on.\u003C/p>\u003C/div>\u003Ch2 id=\"faq\" tabindex=\"-1\">FAQ\u003C/h2>\n\u003Cdiv class=\"post-faq\">\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Should I check $wp_version to decide whether a feature is available?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>No. Ask whether the function or class you need exists with function_exists or class_exists, because the version string tells you what someone claims to run, not what is actually loaded, and plenty of setups backport or strip things.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why check capabilities instead of role names?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Because site owners and membership plugins remap what roles can do. current_user_can(‘manage_affiliates’) survives that setup, while checking whether the user is literally an administrator breaks when a shop grants the same capabilities to a differently named role.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">What should happen when a host is on an unsupported PHP version?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Degrade, do not die. Disable the feature that needs it, keep the core commission tracking working, and show a quiet admin notice, rather than calling wp_die and turning one old host into a support ticket and a one-star review.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Is there ever a case for blocking activation outright?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Yes, at the hard floor below which degrading turns into pretending. Check it in the main plugin file and fail activation cleanly with a clear message and a back link, which is kinder than half-loading into a fatal after install.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">What belongs in a support matrix?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Which WordPress and PHP combinations you fully test and stand behind, which you tolerate with features that may degrade, and which you refuse. Keep it in the readme and enforce the bottom row in code.\u003C/p>\u003C/div>\u003C/div>\u003C/div>","\n## TL;DR\n\nYou cannot control the WordPress and PHP versions your plugin lands on. Detect capabilities at runtime with function_exists and current_user_can, degrade gracefully when something is missing, and publish a support matrix with a hard floor enforced at activation.\n\nThe hardest constraint in plugin work is that you ship one codebase to environments you do not control and cannot see. My affiliate, licensing, and link plugins run on WordPress installs from current down to versions years old, on PHP from modern down to whatever the cheap host froze on, next to a thousand other plugins that also monkey with the same hooks. I cannot pin any of it. That same lack of control is why I [never rewrite one of these plugins in a single big push](/blog/you-dont-rewrite-a-plugin-with-a-big-install-base), because the install base is too varied to bet on. So the strategy is not \"require the latest,\" it is: detect what exists at runtime, degrade gracefully when it does not, and publish a support matrix so both I and the user know where the line is.\n\nHere is how each of those three pieces works in practice.\n\n## Detect capabilities at runtime, do not assume versions\n\nChecking `$wp_version` to decide whether a feature is available is the wrong instinct. The version string tells you what someone claims to run, not what functions and classes are actually loaded, and plenty of setups backport or strip things. The reliable question is not \"which version is this\" but \"does the thing I need exist right now.\" So I ask directly.\n\n```php\nfunction affapp_can_use_block_bindings() {\n    // Introduced in WP 6.5. Ask for the function, not the version.\n    return function_exists( 'register_block_bindings_source' );\n}\n\nif ( affapp_can_use_block_bindings() ) {\n    register_block_bindings_source( 'affapp/referral-url', array(\n        'label'              => __( 'Referral URL', 'affapp' ),\n        'get_value_callback' => 'affapp_referral_binding_value',\n    ) );\n}\n```\n\nThe same discipline covers PHP itself. Before I use a language feature or standard-library function that is not universally present, I check for it rather than assume the interpreter has it.\n\n```php\n// str_contains is PHP 8+. Some hosts are still on 7.4.\nif ( ! function_exists( 'str_contains' ) ) {\n    function str_contains( $haystack, $needle ) {\n        return '' === $needle || false !== strpos( $haystack, $needle );\n    }\n}\n```\n\n`function_exists`, `class_exists`, and `method_exists` are how I write code that is honest about its environment. It asks the room what is present and acts on the answer, instead of trusting a version number that may be lying or backported.\n\n## Capability checks, not role name checks\n\nThe same \"ask, do not assume\" rule applies to permissions. WordPress lets site owners, membership plugins, and other code remap what roles can do. Checking whether the current user is literally an `administrator` is fragile, because a shop might grant an \"affiliate manager\" role the exact capabilities without giving it that role name. So I check the capability, and I register my own where a built-in one does not fit.\n\n```php\n// Wrong: assumes the role name and the capability are the same thing.\nif ( in_array( 'administrator', wp_get_current_user()->roles, true ) ) { }\n\n// Right: ask whether this user can do this specific thing.\nif ( current_user_can( 'manage_affiliates' ) ) {\n    affapp_render_admin_panel();\n}\n```\n\nI add `manage_affiliates` to the roles I ship for on activation, and site owners can grant it wherever they like. The plugin never asks \"who are you,\" it asks \"are you allowed to do this,\" which is the only question that survives contact with a real site's permission setup.\n\n## Graceful degradation over hard requirements\n\nWhen a capability is missing, the plugin should do less, not die. A hard `wp_die()` on an old PHP version turns one unsupported host into a support ticket and a one-star review. Degrading means the core feature still works and the enhancement is simply absent, ideally with a quiet notice so the site owner knows why.\n\n```php\nadd_action( 'admin_init', function () {\n    if ( version_compare( PHP_VERSION, '7.4', '\u003C' ) ) {\n        // Do not fatal. Disable the feature that needs it, tell the owner.\n        add_option( 'affapp_advanced_reports', 0 );\n        add_action( 'admin_notices', 'affapp_php_notice' );\n    }\n} );\n\nfunction affapp_php_notice() {\n    echo '\u003Cdiv class=\"notice notice-warning\">\u003Cp>';\n    echo esc_html__(\n        'Advanced reports need PHP 7.4 or newer. The rest of the plugin works normally.',\n        'affapp'\n    );\n    echo '\u003C/p>\u003C/div>';\n}\n```\n\nThe reason this matters more on a plugin than an app is that the user did not choose their PHP version for you. They are on a host, the host set the version, and moving hosts is a project. My job is to keep the core commission tracking working on their old box while the shiny report is politely unavailable, not to hold their whole plugin hostage to a version bump they cannot make this week. This is the same instinct I bring to [shipping a feature into a five-year-old plugin](/blog/shipping-a-feature-into-a-five-year-old-plugin), where the new capability arrives behind a check and the old path keeps working untouched.\n\nThere is a floor, though. Below some version I will not test against, degrading turns into pretending. That is what the matrix is for.\n\n## Publish a support matrix so the line is explicit\n\nA support matrix is a written statement of which combinations I test and stand behind, which I tolerate, and which I refuse. It protects the user from silent breakage and protects me from being on the hook for a PHP 5.6 box I have not run in years. I keep it in the readme and in my head, and I enforce the hard floor in code.\n\n| WordPress | PHP | Status |\n|-----------|-----|--------|\n| Current and one back | 8.1+ | Fully tested, all features |\n| Two to four back | 7.4 to 8.0 | Supported, advanced features may degrade |\n| Older than that | 7.4+ | Best effort, core only, no guarantees |\n| Any | Below 7.4 | Not supported, activation blocked |\n\nThe bottom row is enforced, not aspirational. I check it in the main plugin file before anything else loads, and I fail the activation cleanly rather than half-loading into a fatal.\n\n```php\nregister_activation_hook( __FILE__, function () {\n    if ( version_compare( PHP_VERSION, '7.4', '\u003C' ) ) {\n        deactivate_plugins( plugin_basename( __FILE__ ) );\n        wp_die(\n            esc_html__( 'This plugin requires PHP 7.4 or newer.', 'affapp' ),\n            esc_html__( 'Plugin activation error', 'affapp' ),\n            array( 'back_link' => true )\n        );\n    }\n} );\n```\n\nBlocking activation on the hard floor is kinder than letting it install and fatal later, because the user gets a clear message at the moment they act, with a back link, instead of a white screen after the fact.\n\nThe matrix is not static. When a new WordPress or PHP release changes behavior I depend on, I update the matrix before I touch the code, adjust the runtime checks, and, when the change reaches stored data, roll it out the same careful way I handle [backward-compatible migrations on an install base](/blog/backward-compatible-migrations-on-an-install-base).\n\n## The strategy in one line\n\nAsk the environment what it can do, do less when it can do less, and write down where you stop. `function_exists` and `current_user_can` over version and role assumptions. A notice and a disabled feature over a fatal. A published matrix with an enforced floor over silent hope. I cannot control the WordPress and PHP versions my plugins land on, and I stopped trying. The whole compatibility strategy is accepting that I do not control the room, and writing code that behaves well no matter which room it wakes up in.\n\n## The takeaway\n\nCompatibility across versions you cannot see comes down to honesty about the environment. Ask what functions, classes, and capabilities are actually present, do less when the answer is less, and block activation cleanly below the floor you are willing to test. A runtime check beats a version assumption, a disabled feature with a notice beats a white screen, and a published matrix with an enforced floor beats silent hope on every host your plugin wakes up on.\n\n## FAQ\n\n**Should I check $wp_version to decide whether a feature is available?**\nNo. Ask whether the function or class you need exists with function_exists or class_exists, because the version string tells you what someone claims to run, not what is actually loaded, and plenty of setups backport or strip things.\n\n**Why check capabilities instead of role names?**\nBecause site owners and membership plugins remap what roles can do. current_user_can('manage_affiliates') survives that setup, while checking whether the user is literally an administrator breaks when a shop grants the same capabilities to a differently named role.\n\n**What should happen when a host is on an unsupported PHP version?**\nDegrade, do not die. Disable the feature that needs it, keep the core commission tracking working, and show a quiet admin notice, rather than calling wp_die and turning one old host into a support ticket and a one-star review.\n\n**Is there ever a case for blocking activation outright?**\nYes, at the hard floor below which degrading turns into pretending. Check it in the main plugin file and fail activation cleanly with a clear message and a back link, which is kinder than half-loading into a fatal after install.\n\n**What belongs in a support matrix?**\nWhich WordPress and PHP combinations you fully test and stand behind, which you tolerate with features that may degrade, and which you refuse. Keep it in the readme and enforce the bottom row in code.\n",[22,25,28,31,34],{"q":23,"a":24},"Should I check $wp_version to decide whether a feature is available?","No. Ask whether the function or class you need exists with function_exists or class_exists, because the version string tells you what someone claims to run, not what is actually loaded, and plenty of setups backport or strip things.",{"q":26,"a":27},"Why check capabilities instead of role names?","Because site owners and membership plugins remap what roles can do. current_user_can('manage_affiliates') survives that setup, while checking whether the user is literally an administrator breaks when a shop grants the same capabilities to a differently named role.",{"q":29,"a":30},"What should happen when a host is on an unsupported PHP version?","Degrade, do not die. Disable the feature that needs it, keep the core commission tracking working, and show a quiet admin notice, rather than calling wp_die and turning one old host into a support ticket and a one-star review.",{"q":32,"a":33},"Is there ever a case for blocking activation outright?","Yes, at the hard floor below which degrading turns into pretending. Check it in the main plugin file and fail activation cleanly with a clear message and a back link, which is kinder than half-loading into a fatal after install.",{"q":35,"a":36},"What belongs in a support matrix?","Which WordPress and PHP combinations you fully test and stand behind, which you tolerate with features that may degrade, and which you refuse. Keep it in the readme and enforce the bottom row in code.","8 min read",8,1785070532920]