Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Plugin Update Checker Laravel Package

yahnis-elsts/plugin-update-checker

A PHP library that lets WordPress plugins and themes check for updates from GitHub, GitLab, Bitbucket, or a custom update server. It hooks into the native WordPress update system so users see and install new versions just like plugins hosted on WordPress.org.

View on GitHub
Deep Wiki
Context7

Getting Started

Where to look first. The file you actually need is plugin-update-checker/plugin-update-checker.php inside the repo; it bootstraps the YahnisElsts\PluginUpdateChecker\v5 namespace. Ignore the languages/, js/, css/ folders on day one — they exist for the plugin-info modal and settings UI. The canonical entry point is the PucFactory class:

\YahnisElsts\PluginUpdateChecker\v5\PucFactory

If you see tutorials referencing PucFactory without a namespace or Puc_v4_Factory, they are for the deprecated v4 line — v5 lives under the v5 namespace and the old names are aliased but discouraged.

Minimal setup (self-hosted JSON). Drop the library into your plugin, require it once, and point it at a metadata file:

require_once __DIR__ . '/vendor/plugin-update-checker/plugin-update-checker.php';

use YahnisElsts\PluginUpdateChecker\v5\PucFactory;

$checker = PucFactory::buildUpdateChecker(
    'https://updates.example.com/my-plugin/metadata.json',
    __FILE__,                 // plugin main file
    'my-plugin',              // slug, must match the plugin directory
    12                        // hours between checks
);

Minimal setup (GitHub). GitHub, GitLab and Bitbucket URLs are auto-detected:

$checker = PucFactory::buildUpdateChecker(
    'https://github.com/acme/my-plugin/',
    __FILE__,
    'my-plugin'
);
$checker->setBranch('main');                    // required for GitHub; defaults to master
$checker->getVcsApi()->enableReleaseAssets();   // ship the ZIP from GitHub Releases

First use case to validate. You have a plugin installed from a ZIP, you bump Version: in the plugin header, tag/push a release, and you want WP's Dashboard → Updates to offer the new version. Verify by (1) confirming the plugin header version is lower than the release version, (2) loading any admin page, (3) visiting Dashboard → Updates. If nothing shows, force a check:

delete_site_transient('update_plugins');
wp_update_plugins();   // or navigate to Dashboard → Updates

For themes the same factory works: pass __FILE__ as the style.css path (or the theme's functions.php) and buildThemeUpdateChecker() is a convenience wrapper. The version is read from the Version: header in style.css.

Implementation Patterns

Bundle the library, don't require it via Composer at runtime in production. The library is tiny and self-contained; shipping it inside your plugin (e.g. vendor/plugin-update-checker/) avoids a chicken-and-egg problem — if the update checker itself is a dependency, it can't update itself. If you do use Composer, composer require yahnis-elsts/plugin-update-checker and require vendor/autoload.php, but commit vendor/ or ship the built artifact. Never let the checker load conditionally on something that might not exist yet.

Guard the factory call once, not per admin request. The library registers filters, cron events and an init hook that does the check on a schedule. Calling buildUpdateChecker() twice with the same slug creates duplicate API calls and can thrash the transient. Structure your bootstrap so a single checker is constructed:

add_action('plugins_loaded', function () {
    if (!class_exists('YahnisElsts\PluginUpdateChecker\v5\PucFactory')) {
        return;
    }
    // ... build once, store in a static or global
});

GitHub release assets pattern. enableReleaseAssets() makes the update checker look for a .zip attached to the GitHub Release. The zip's top-level folder must be exactly your slug (my-plugin/), or WordPress will install it as my-plugin-1/ and you'll get a duplicate. If your asset naming doesn't follow the convention, filter puc_github_release_assets — the callback receives [ $assets, $api, $checker ] and must return the list. Note the branch is still used to find the current version header; the release asset is only the download.

Private repositories. Use a token with repo scope; never commit it:

$checker->setAuthentication(get_option('acme_my_plugin_gh_token'));

Store tokens in an option you set from an admin settings page, and mask them in any debug output. The same setAuthentication() works for GitLab (personal access token, read_api scope) and Bitbucket (app passwords).

Custom metadata JSON. For fully self-hosted plugins, the metadata file is the contract. Minimum fields WP displays or uses:

{
  "name": "My Plugin",
  "slug": "my-plugin",
  "version": "1.4.2",
  "download_url": "https://updates.example.com/my-plugin/my-plugin-1.4.2.zip",
  "requires": "6.0",
  "tested": "6.6",
  "requires_php": "7.4",
  "author": "Acme",
  "homepage": "https://example.com",
  "sections": { "description": "...", "changelog": "..." },
  "banners": { "low": "...", "high": "..." }
}

slug and download_url are the two that break silently if wrong. download_url must point to a ZIP whose root folder is my-plugin/.

Debug and manual checks from code. You can trigger and inspect the checker without waiting for cron:

$checker->checkForUpdates();
$state = $checker->getUpdateState();          // Puc_v5_UpdateState
$update = $state->getUpdate();

Wrap checkForUpdates() in a transient/flag so you don't hammer the API on a page reload.

Filtering and rewriting requests. The extension points most people need:

$checker->addQueryArgFilter(function ($args) {
    $args['license_key'] = get_option('acme_license');
    return $args;
});

$checker->addHttpRequestArgFilter(function ($args) {
    $args['timeout'] = 30;   // slow update servers
    return $args;
});

$checker->addResultFilter(function ($meta, $httpResult) {
    $meta->icons = ['1x' => 'https://cdn.example.com/icon.png'];
    return $meta;
});

addFilter() gives you general plugin filters if you need to intercept the version header (puc_plugin_version) or the file list.

Theme equivalents. PucFactory::buildThemeUpdateChecker($url, $styleCssPath, $slug) is the shorthand. Header parsing is on Version: in style.css; the ZIP layout rule (top-level folder equals theme slug) still applies. The sections in your JSON populate the "View version details" modal.

Gotchas and Tips

Slug/folder mismatch is the #1 silent failure. The third argument to buildUpdateChecker() must match the directory name of the installed plugin. WordPress maps updates by slug. A mismatch gives you an "Update available" prompt that installs a second copy (or fails with "Destination folder already exists"). When debugging, var_dump($update->slug) on the Puc_v5_Update object.

Version comparison is header-based. For GitHub/Bitbucket, the checker reads Version: from the plugin file on the branch, not from the release tag. If you forget to bump Version: before tagging, no update appears even though the tag is newer. The tag name itself is ignored for versioning.

Cron and transients fight you during testing. Three layers of caching stand between you and a visible update: the update_plugins site transient, the package's own cache transient (puc_external_updates-{slug}), and the scheduled cron (puc_cron_check_updates-{slug}). The reliable nuke:

delete_site_transient('update_plugins');
delete_transient('puc_external_updates-my-plugin');
wp_clear_scheduled_hook('puc_cron_check_updates-my-plugin');

Set PUC_DEBUG (and PUC_DEBUG_LOG) to true before the library loads to get request/response traces into the debug log (PUC_DEBUG echoes to page, PUC_DEBUG_LOG writes to debug.log). Remember WP_DEBUG alone is not enough.

GitHub rate limiting bites unauthenticated sites. Unauthenticated GitHub API calls are ~60/hour per IP. Shared hosting IPs and multisite installations hit this fast. Always call setAuthentication() for production GitHub use, even on public repos — the token just needs public_repo scope. If you don't, scheduled cron updates will start returning 403 and updates will silently stall (the checker logs the failure, so PUC_DEBUG_LOG is your friend).

setBranch() defaults to master. New GitHub repos default to main. If you forget, the checker will report "could not get plugin information" or silently find no version. This is easy to miss because the code doesn't throw — it just returns null.

Private repo + public plugin info page. If you rely on GitHub for metadata but the repo is private, the "View version details" modal fails because assets/icons/readme are fetched from the same API. Either host a static JSON metadata file yourself (set setAuthentication() only for the download URL) or accept the modal's fallback.

Multiple plugins in one site, one library copy. The library refuses to load twice (it has a class_exists guard), but if you're shipping the same copy in multiple plugins, the one that loads first wins — and the others' buildUpdateChecker() calls will use that first-loaded version. This is usually fine but means you cannot pin different versions per plugin. Prefer bundling in exactly one plugin and using a shared mu-plugin, or rely on the "load the newest copy" convention (the library has a self-update guard you should understand before splitting).

Don't call the checker on every request. The default check period is 12 hours and it's driven by WordPress cron. If your host disables WP-Cron (DISABLE_WP_CRON), updates never run. Either add a real system cron hitting wp-cron.php or set a shorter period — but never set it to 0 "to test"; that turns every admin page load into an outbound HTTP request and will get you rate-limited or flagged by your object cache.

download_url must serve a real ZIP with a proper root folder. If your server streams the file with the wrong Content-Type, or the ZIP was created with a top-level directory named after the version (my-plugin-1.4.2/), WordPress will install to a duplicate folder. Repackage so the ZIP root is my-plugin/. Verify with unzip -l my-plugin-1.4.2.zip.

Token leakage. It is tempting to hardcode a GitHub token "just for the private repo". That token ends up in the plugin ZIP every customer downloads. If you need a private-repo pattern, serve a signed download URL from your own server and keep the token there; the checker only needs the URL.

Extension points worth knowing. getVcsApi() returns the GitHub/GitLab/Bitbucket API object — useful for custom release lookups ($api->fetchRelease()), and enableReleaseAssets() is on that object, not the checker. $checker->removeHooks() (or disableQuerying()) is handy in test harnesses to prevent accidental network calls. buildUpdateChecker() returns a Puc_v5_Plugin_UpdateChecker; themes get a Puc_v5_Theme_UpdateChecker — some filters are plugin-only.

Rollback safety. Because WordPress 5.2+, failed updates auto-restore the previous version via "Rollback". This library is compatible, but if you push an update that fatals in the plugin header (parse error), the rollback happens on next request — users see a white screen briefly. Test the packaged ZIP in a staging WP install, not just php -l on individual files.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log