hexadog/laravel-theme-installer
Composer installer for Laravel theme packages that automatically places themes in the correct themes directory. Integrates with hexadog/laravel-themes, simplifying theme distribution and installation without manual publishing or path configuration.
The installer is a Composer plugin, not a runtime library. It changes where Composer puts theme packages: any package whose composer.json declares "type": "laravel-theme" is installed into themes/ in your project root instead of vendor/. Nothing it does is visible at request time; if you are debugging a theme at runtime, you are debugging hexadog/laravel-theme, not this package.
Minimal setup, in order:
composer require hexadog/laravel-theme-installer
Composer 2.2+ will prompt you to allow the plugin (or refuse it in non-interactive mode). Accept it, or add it to composer.json up front:
{
"config": {
"allow-plugins": {
"hexadog/laravel-theme-installer": true
}
}
}
Then pull in a theme:
composer require hexadog/laravel-theme-<name>
ls themes/ # the theme lives here, not in vendor/
Where to look first:
composer.json → config.allow-plugins (the single most common reason "it didn't work").composer.json → confirm "type": "laravel-theme". If the type is library, the plugin ignores it and it lands in vendor/.config/themes.php in the host hexadog/laravel-theme package) → the path the runtime expects must match the path the installer used.First real use case: you want to edit a theme's Blade views, SCSS and assets without fighting vendor/ (which is gitignored, wiped on composer install, and never meant to be edited). Install this plugin, and themes become first-class project files you can commit, diff and review.
Treat themes/ as source, vendor/ as build output. This is the whole point of the package. Once a theme is installed outside vendor/, commit themes/ and keep vendor/ ignored. Your deploy diff then shows theme changes like any other code change.
Customise by forking the theme, not by patching it in place. Two viable patterns:
composer.json at your fork ("repositories": [{ "type": "vcs", "url": "[email protected]:you/theme-x.git" }]), and composer require you/theme-x. Upstream updates arrive via merge, not via composer update clobbering your edits.resources/views/vendor/<theme> (the standard Laravel package override convention). Nothing in themes/ is edited, so composer update stays safe.Pick one per theme and be consistent — mixing them is how teams lose customisations.
Pin the theme, float the installer. Theme packages are effectively application code once they live in your repo; pin them with exact versions or a narrow constraint. The installer is tooling — a caret constraint is fine:
"require": {
"hexadog/laravel-theme-installer": "^1.0",
"hexadog/laravel-theme-adminlte": "2.3.*"
}
CI and containers. The plugin must run during composer install, so:
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-scripts --prefer-dist
Do not add --no-plugins here. If your base image or a security policy globally disables plugins, whitelist this one explicitly. Also ensure the build user can write to the project root (themes/ is created there), not just to vendor/.
Deployment ordering. Because the install target is inside the release directory and outside vendor/, a zero-downtime deploy that symlinks vendor/ from a shared directory will not share themes/. Either build themes into each release (normal composer install per release) or explicitly symlink themes/ alongside vendor/ — but then remember theme updates require a composer install in the shared location.
Multi-theme / multi-tenant. Installing several theme packages is fine — each gets its own directory under themes/. Activate per environment or per tenant via the host package's config or a runtime Theme::set(). Keep theme selection in config/env, never in committed code paths.
Local path repositories during theme development. While actively developing a theme, use a path repository so themes/ symlinks to your working copy:
"repositories": [
{ "type": "path", "url": "../my-theme", "options": { "symlink": true } }
]
The installer will still place it under themes/, which keeps your app's view resolution identical between dev and prod.
allow-plugins is the #1 failure. Symptom: composer require succeeds, exit code 0, but the package is in vendor/ and themes/ was never created. Composer silently falls back to the default installer when a plugin isn't allowed. Run with -vvv and look for "plugin ... was not allowed" or the Installing <pkg> (type: laravel-theme) line — if you see it going to vendor/, this is why.
--no-plugins breaks everything, quietly. Any composer install --no-plugins, --no-scripts+plugin-disabled combo, or a hardened CI image that strips plugins will install themes into vendor/ and leave themes/ stale or absent. The autoloader may still resolve the old path, so you get a mix of old and new theme files rather than a clean error. Grep your build scripts for --no-plugins.
Package type is the contract. The plugin only intercepts packages with "type": "laravel-theme". If you fork a theme and drop or rename the type field in the fork's composer.json, it silently reverts to vendor/. Same if upstream renames the type in a major release — pin and read the changelog.
Changing the install path later is not a no-op. The path is baked into composer.lock's install metadata and into the generated autoloader. After changing the configured theme directory (or upgrading the installer across a path-affecting release), do a clean cycle:
rm -rf vendor themes composer.lock
composer install
Otherwise Composer believes the package is already installed at the old location and skips the move.
composer remove leaves the directory behind. Uninstalling a theme removes the lock entry but frequently leaves themes/<name>/ on disk, because the plugin's uninstall path handling can't safely distinguish "installed by me" from "you edited files here". Delete manually and commit the removal — don't let orphaned theme directories sit in the repo.
.gitignore audit. Most Laravel .gitignore files are fine, but check for a broad themes/ or /themes entry inherited from a starter kit, and for *.css/*.js ignores under public/ that would swallow built theme assets. Conversely, if you don't want to commit themes, make that an explicit decision — the package's value proposition assumes you do.
Autoloading is the plugin's problem, until it isn't. Composer generates autoload maps from the install path, so a correctly-run plugin gives you working PSR-4 for themes outside vendor/. If class-not-found errors appear right after install, the usual culprit is a stale autoloader: composer dump-autoload (with plugins enabled). Running dump-autoload --no-plugins can regenerate the map against the default vendor/ path and break resolution.
Path resolution is relative to the Composer invocation directory. Running composer install from a subdirectory or with COMPOSER=<path> moves the effective project root; themes/ may be created somewhere unexpected. Always run Composer from the directory containing composer.json.
Case sensitivity and Windows. Theme directories are derived from the package name (vendor/name → directory name), and the mapping has historically been inconsistent between releases. On case-insensitive filesystems this works locally and fails on Linux CI. Verify the actual directory name after install rather than assuming.
Config key drift. The override for the install directory lives in the root composer.json's extra block, but the exact key has changed between releases. Copy the snippet from the README matching your installed version rather than from a blog post, and confirm with composer config extra / composer show hexadog/laravel-theme-installer.
Debugging recipe. When a theme ends up in the wrong place:
composer show hexadog/laravel-theme-<name> # shows path + type
composer install -vvv 2>&1 | grep -i -E 'theme|installer|plugin'
composer diagnose
composer show printing a path under vendor/ is definitive: the plugin did not run or did not match the type.
Extension point. The plugin is intentionally tiny — its only real surface is the install path and the package type it responds to. If you need custom placement logic (per-tenant theme directories, external asset volumes), don't fight it; install normally and move/symlink in your deploy step.
How can I help you explore Laravel packages today?