akaunting/module
A Laravel package that adds a modular architecture to Akaunting, letting developers package features as self-contained modules with service providers, routes, views, migrations, and settings. It simplifies registering, installing, enabling, and managing modules within an Akaunting application.
Install the package alongside its Laravel dependencies:
composer require akaunting/module
Publish the config and run the shipped migration so the modules table exists (this table stores the enabled/disabled state and installed version of each module):
php artisan vendor:publish --provider="Akaunting\Module\Providers\Laravel"
php artisan migrate
The first place to look is config/module.php. It defines where modules live (default modules/ at the project root), the path pattern, cache behaviour, and the auto-discovery options. Then scaffold your first module:
php artisan module:make Blog
This creates modules/Blog/ with a module.json manifest, a Providers/ directory, and stubs for routes/config/views. Enable it:
php artisan module:enable Blog
php artisan module:list
Your first real use case is almost always: "add a self-contained feature (billing, exports, an admin panel) that I can toggle on and off per environment without touching composer.json and without redeploying a dependency." The package gives you that toggle via Modules::isEnabled('blog') and the corresponding DB row.
The module.json manifest is the source of truth for bootstrapping. A minimal manifest:
{
"name": "Blog",
"alias": "blog",
"description": "Blog module",
"version": "1.0.0",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Blog\\Providers\\BlogServiceProvider"
],
"aliases": {},
"files": [],
"requires": []
}
providers are registered at boot (only when enabled), aliases are registered into the class alias loader, and files are required — don't put side-effecting code in files unless you really need global helpers.
Gate code on module state rather than on class existence. Use the Modules facade (or the injected Akaunting\Module\Contracts\Repository):
use Akaunting\Module\Facades\Module as Modules;
if (Modules::isEnabled('blog')) {
// register routes, menus, etc.
}
$blog = Modules::get('blog'); // Module instance or null
$all = Modules::all(); // all installed modules
$on = Modules::enabled();
This is how you implement "feature flag by package" without wiring in a separate flag system.
Toggle from an admin UI. The enable/disable operations are idempotent; wrap them in a controller and call $module->enable() / $module->disable(), or shell out to the artisan commands. Remember to clear the module cache after (see Gotchas).
Depend on other modules. The requires array in module.json lists aliases; install/enable will fail loudly if a dependency is missing or disabled. Use it to model optional features that compose — e.g. Reporting requires Blog.
Version and upgrade flow. The modules table has version; compare it to the manifest's version to decide whether to run a module's migrations or upgrade script. This is the intended way to ship schema changes inside a toggleable module.
Keep providers thin. The package only knows how to load your provider; registration order, route prefixes, and middleware are your job. A typical module provider does:
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../Config/blog.php', 'blog');
}
public function boot()
{
$this->loadRoutesFrom(__DIR__.'/../Routes/web.php');
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'blog');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
$this->publishes([
__DIR__.'/../Config/blog.php' => config_path('blog.php'),
], 'config');
}
config/module.php says "where to look." All three can drift. If module:list disagrees with reality it's usually a stale row — the module:disable/module:enable commands are safe to re-run to reconcile.php artisan cache:clear (or the package's clear command) or you'll chase a ghost where a disabled module's routes still respond. In CI, clear cache as part of every deploy.composer.json. If a class isn't found after module:make, check the namespace prefix in the manifest matches the directory and that you actually composer dump-autoloadd (the package still relies on the base autoloader for shared deps).register() via $this->app->extend(...) rather than assuming your binding wins.module.json (trailing comma, wrong providers path) throws during the service provider bootstrap, and the error often points at the framework rather than the module. When a fresh clone explodes before any route matches, json_decode the manifest manually first.aliases becomes a global class alias — two modules declaring the same alias will silently shadow each other depending on priority. Use unique, namespaced aliases (BlogFacade not Blog).files. The files array is required on every request; keep it to helper functions, never registrations or heavy bootstrapping. Prefer providers.requires is checked at install/enable, not continuously. Disabling a module that another enabled module requires may leave the dependent in a broken state — enforce that in your admin UI.modules/. module:install, module:make, and publish commands write to disk. On immutable or read-only production filesystems, treat modules as build artifacts: install locally, commit modules/, and only enable in production.Modules::get('blog') returning null while module:list shows it is nearly always a cache issue or a path/namespace mismatch in config/module.php. Check both before touching the manifest.setUp ($this->artisan('cache:clear') or the package command) and use Modules::isEnabled() guards in test helpers so tests can run with the module off without fatal class-not-found errors.How can I help you explore Laravel packages today?