puli/manager
Puli Manager provides the command-line tools for Puli, a PHP resource management system. It lets you initialize and configure repositories, map package resources, publish assets, and inspect resource paths across Composer packages.
Puli Manager is the low-level library behind the puli CLI. It owns reading, validating, mutating and persisting the project’s puli.json file and the derived .puli/ directory. Day to day you will almost always reach for it through puli/cli; the programmatic API is for build scripts, Composer hooks and custom tooling.
Minimum viable setup:
composer require --dev puli/cli # pulls in puli/manager
vendor/bin/puli init # creates puli.json + .puli/
If you only want the library:
composer require puli/manager
Where to look first, in order:
Puli\Manager\Api\Puli — the facade. Everything hangs off it: getConfigManager(), getPackageManager(), getBindingManager(), getDiscoveryManager(), getServerManager(), getModuleManager(), getModuleList(), getContext().puli.json in the project root — the single source of truth for config, packages and bindings..puli/ — generated state (discovery.json, path mappings, install info). Treat as a cache; never hand-edit.Puli\Manager\Api\Event\PuliEvents — the extension surface if you need to hook install/load lifecycle.Puli\Manager\Api\Config\Config, Api\Package\PackageManager, Api\Binding\BindingManager — the manager classes you will call in 90% of scripts.First real use case — declare that a package’s assets are published under a URL path, then query them:
use Puli\Manager\Api\Puli;
$puli = Puli::create(base_path()); // bootstraps puli.json + .puli
$puli->getPackageManager()->loadPackages();
$puli->getBindingManager()->addBinding(
\Puli\Discovery\Api\Binding\Binding::forPath('/app/public', '/app/assets')
);
$puli->getBindingManager()->saveBindings();
$resources = $puli->getDiscoveryManager()->find('/app/assets/**/*.css');
That is the whole shape of the package: mutate a manager, then call the matching save…() method. There is no auto-save, no facade, no Laravel integration of any kind.
Puli behind a container singletonPuli::create() parses and validates config; don’t call it per-request. Register it once:
// AppServiceProvider::register()
$this->app->singleton(\Puli\Manager\Api\Puli::class, function ($app) {
return \Puli\Manager\Api\Puli::create($app->basePath());
});
Inject the manager you actually need (PackageManager, BindingManager, DiscoveryManager) rather than the facade, so unit tests can mock it.
Most day-to-day mutations are one-off operations that belong in a command rather than HTTP:
$puli->getPackageManager()->installPackage('acme/assets', 'vendor/acme/assets', 'composer');
$puli->getPackageManager()->savePackages();
$puli->getDiscoveryManager()->clearDiscovery(); // invalidate cache after changes
Expose these as php artisan puli:bind {path} {query} and similar. This is the cleanest way to keep puli.json under version control while letting developers change it.
Package install/remove is naturally a Composer event. Add to composer.json:
"scripts": {
"post-install-cmd": ["vendor/bin/puli build"],
"post-update-cmd": ["vendor/bin/puli build"]
}
For custom behavior, subscribe to PuliEvents::POST_INSTALL_PACKAGE / POST_REMOVE_PACKAGE on $puli->getContext()->getEventDispatcher() instead of reimplementing package discovery.
ConfigManager, not json_decodeConfig is layered: root puli.json, each package’s puli.json, and extra.puli in composer.json. ConfigManager::getConfig() gives you the merged view with package-namespaced parameters (puli.asset-url-format, etc.):
$config = $puli->getConfigManager()->getConfig();
if ($config->hasParameter('puli.asset-url-format')) {
$format = $config->getParameter('puli.asset-url-format');
}
Use cloneConfigFile() / addConfigFile() when you need to override layered config in a build, and always follow with saveConfig().
DiscoveryManager::find($query) is the read side of bindings; findInDirectory() and findInPhpServer() are convenience variants. A typical asset publishing step is:
foreach ($puli->getDiscoveryManager()->find('/app/assets/**/*') as $resource) {
copy((string) $resource->getFilesystemPath(), public_path(...));
}
Bindings are keyed by their path, so getBinding($path) / removeBinding($path) are the safe idempotent update primitives — check hasBinding() before adding to keep scripts re-runnable.
ServerManager and ModuleManager are how Puli exposes per-framework URL formats (puli/twig-extension, puli/symfony-bridge). In a Laravel app you would not use these — you would register a custom Server or simply call asset() yourself — but if you are porting a Symfony bundle integration, Server::forName(...)->getUrlFormat() is where the asset URL pattern lives.
Commit puli.json; gitignore .puli/. Rebuild it in CI with puli build, otherwise discovery.json will drift from the bindings and find() will return stale resources.
composer resolution, deprecated constructs in transitive webmozart/* packages). Pin PHP or vendor the relevant classes if you must use it on modern runtimes.puli/manager gives you the API; puli/cli gives you vendor/bin/puli. Documentation and examples online are almost all CLI-oriented, so translate commands into manager calls (puli bind → BindingManager::addBinding).addBinding/installPackage/setParameter needs the corresponding saveBindings()/savePackages()/saveConfig(). Forgetting the save is the number one silent failure — the in-memory state looks right for the rest of the request, then disappears.puli.json while a manager instance is live. The managers hold a parsed config; a later save…() will overwrite your manual edit. Route all writes through the API.PackageManager::installPackage) before binding it.composer.json. Puli::create() will fail confusingly if the root composer.json is missing or has no name. In a Laravel app that means composer.json must be at base_path().find() empty?” After any binding or package change call clearDiscovery() (and saveDiscovery() when the build is done). Conversely, deleting .puli/ forces a clean rebuild.getConfigFile() is the root file, but getConfig() is merged. Overrides from package puli.json and extra.puli can shadow what you set at root — use isCloned()/cloneConfigFile() when a build needs a deterministic single-file view.Puli\Manager\Api\Config\ConfigException, Api\Package\PackageException, Api\Binding\BindingException — catch these explicitly in commands and surface the message; they are precise and human-readable (better than the generic Puli CLI exit codes).puli.json. Do not run puli mutations in parallel CI jobs against the same checkout; serialize them.final and built around PuliEvents. If you need custom behavior (e.g. copying assets on package install), add a listener on POST_INSTALL_PACKAGE rather than extending a manager.extra block. Use puli/manager only when maintaining a legacy project that already has puli.json and a puli build step.How can I help you explore Laravel packages today?