Skip to content

DACore · coming December 2026

How to write a DACore module

After DACore is installed, a programmer should spend time on the product, not on login. The module still has rules. It must register permissions, menu items, and optional AI tools; it must be installable and uninstallable; it must render inside the DACore shell. TestInstalatora is the canonical sample: tables, seed data, rights, nested menu, an admin page, and a symmetric uninstaller — without inventing a second auth stack. Scaffold the PHP classes with DotApper. Then add the DACore contract on top.

Common mistakes

Wrong Right
Hand-write the module class. php dotapper.php --create-module=Shop then add dainstall.php and init/.
Build a custom admin HTML layout. DACore:Page@withMenu! and fragments in views/layouts/.
Register menu and rights in initialize() on every boot. Do it in Installation.php behind Installations@exist!.
Uninstall that forgets dacore_menu or AI tools. Delete your menuid prefix, Rights@deleteGroup!, unregister tools, DROP shop_*.
Empty rights on an AI tool. The tool is invisible to everyone. Wildcards do not save you here.

Package anatomy

Shop/
  dainstall.php
  Installation.php
  init/module.init.php
  init/module.listeners.php
  module.init.php          (stub until init/ is copied)
  module.listeners.php
  Controllers/
  Middleware/Rights.php
  views/layouts/
  translations/
  assets/

Table names: shop_*. Menu ids: Shop.*. Permission creator: Shop. Never edit app/parts/ or app/modules/DACore/. Overrides for this environment go in app/config.php. Framework-only rules still apply: in templates, DB::module('RAW'), callable strings with trailing ! on hot paths. See how to create a module for DotApper and fallbacks, then return here for the DACore layer.

Installer callback

Each semver key runs once. Create tables, seed, create the rights group, create rights, optionally assign the installing user, register menu rows, optionally register AI tools, then insert the installation row.

'1.0.0' => function () {
    $version = '1.0.0';
    if (DotApp::call('DACore:Installations@exist!', 'Shop', $version) === true) {
        return;
    }
    // CREATE TABLE shop_* …
    $groupId = DotApp::call('DACore:Rights@createGroup!', 'Shop', 'Shop');
    DotApp::call('DACore:Rights@createRight!', $groupId, 'View orders', '', 'Shop', 'orders.view', 'Shop');
    DotApp::call('DACore:Menu@register', 'Shop.orders.list', [ /* … */ ]);
    DotApp::call(
        'DACore:Installations@insert!',
        'Shop',
        $version,
        1,
        ['outcome' => 'ok']
    );
}

Uninstaller callback

Reverse the same version. Order matters: drop dependents first if you have foreign keys inside your own schema.

DotApp::call('DACore:Rights@deleteGroup!', 'Shop');
// DELETE FROM dacore_menu WHERE menuid LIKE 'Shop.%'
// DACore:AITools@delete for each tool id, or delete by creator
// DROP TABLE IF EXISTS shop_orders, shop_items, …

DACore then deletes installation rows for that module name and removes the folder. If you skip menu cleanup, the next operator sees dead links. If you skip rights cleanup, the catalog lies.

Routes under the admin prefix

In init/module.init.php read Config::module('DACore', 'prefixUrl') and your own pagePath fallback. Load translations. Register GET/POST only when Auth::isLogged() === true, with your Rights middleware in before. List screens that can grow must ship paginate() and an AJAX pager in the first version. A pager that reloads the admin shell is not a pager.

Optional AI tools

If the desk AI is enabled, your module can register tools the assistant is allowed to call. Naming: Shop.Orders.Search. The controller string is a normal callable. Rights are checked again at execution. Empty rights means nobody, including root in practice of the registry, sees a usable tool — fill the array.

DotApp::call('DACore:AITools@register', 'Shop.Orders.Search', [
    'creator' => 'Shop',
    'description' => 'Search orders by number or email.',
    'controller' => 'Shop:AI@searchOrders',
    'howtouse' => json_encode(['input_schema' => ['query' => ['type' => 'string', 'required' => true]]]),
    'rights' => json_encode(['dotapp.root', 'Shop.orders.view']),
    'tool_type' => 'lookup',
    'risk_level' => 0,
    'requires_confirmation' => false,
    'intent_tags' => ['find order', 'search orders'],
]);

Mutating tools set requires_confirmation and a higher risk_level. Use allowed_tools / forbidden_tools so a lookup workflow cannot jump into delete. Unregister on uninstall. Details belong with AIRULES doc 34 and the sample EX-D03. You are not teaching the model to log users in. DACore already did that. You expose your nouns.

Ship checklist

  • dainstall.php calls Installation::module('YourModule')->install()
  • Versioned installer() and uninstaller() with exist/insert guards
  • init/module.init.php and init/module.listeners.php
  • Tables yourmodule_*
  • Rights group + rights + menu ids prefixed with the module name
  • Optional AI tools with non-empty rights
  • Controllers use Page@withMenu!
  • Your own Rights middleware, not AuthTest for permissions
  • Locale JSON for menu and right labels
  • ZIP passes validation (no root install.php)

FAQ

Does DotApper create dainstall.php?

DotApper creates a framework module. You add the DACore files (or copy TestInstalatora). Do not wait for the CLI to guess the admin contract.

Can the same module have a public storefront and a DACore desk?

Yes. Public routes stay in initialize() without the admin prefix. Desk routes sit under prefixUrl and withMenu. Do not mix the public HTML shell with the admin chrome.

Where do I push “order paid”?

DACore:Notifications@push on that event. Not in the installer, not in a cron that fires every request.

See also