schoolpalm / module-sdk
Official SDK for building SchoolPalm modules
Requires
- php: ^8.2
- illuminate/console: ^12.0
- illuminate/support: ^12.0
- inertiajs/inertia-laravel: ^2.0
- opis/json-schema: ^2.6
- schoolpalm/module-bridge: dev-main
Suggests
- @inertiajs/inertia-vue3: JS Vue3 adapter for Inertia
- quasar: Quasar framework for UI components
- vue: Vue3 for module frontend
README
Overview
SchoolPalm Module SDK is the official package used to scaffold, validate, register, and run SchoolPalm modules inside a Laravel application.
At runtime, requests are routed through the SDK, the requested module is resolved/initialized, and the module executes an action (e.g. view-list, create, edit).
The SDK integrates with schoolpalm/module-bridge to:
- register module namespaces/providers dynamically from a registry file
- build and expose a module relation registry (used for resolving pipelines/relations between modules)
- provide shared utilities/config used by both packages
In this document, the term module refers to a SchoolPalm module created with the SDK scaffolding tools and executed via the SDK runtime.
What problem it solves
The SDK provides:
- Module lifecycle tooling (CLI):
- create a module manifest (
sp:make-m) - scaffold folder structure from manifest (
sp:gen-s) - validate module manifests against a JSON schema (
sp:validate-mviaManifestValidator) - install module assets/route wiring (
sp:install)
- create a module manifest (
- Runtime execution layer:
- Laravel HTTP endpoints that resolve a module and call the correct action
- Inertia rendering for module dashboards
- Action dispatch:
- translate route
action→ method namerunX()and/or delegate toActionsclasses
- translate route
Repository layout (key entrypoints)
Within src/ the most relevant parts are:
ModuleSDKServiceProvider.php— SDK service provider; merges config and registers console commandssrc/routes/vendor_routes.php— dynamic route definition for module executionsrc/http/ModuleController.php— runtime resolver + action execution + dashboard renderingsrc/http/SDKController.php— additional controller endpoints (module host/API entrypoint)src/core/ActionResolver.php— mapsaction→runX()and delegates toActions/*classessrc/manifest/*—ManifestFactory,ManifestValidatorsrc/console/*— CLI commands (scaffold, validate, install, etc.)
module-bridge integration (core mechanism)
Module registration
Integration is primarily done through:
SchoolPalm\ModuleBridge\Providers\ModuleRegistrar
The SDK service provider calls:
ModuleRegistrar::registerSnapshots(...)ModuleRegistrar::registerModules(...)ModuleRegistrar::bootRelations(...)
These are invoked inside:
src/ModuleSDKServiceProvider.php:
- merge
sdk.phpconfig from SDK itself and from module-bridge helper/config - register module autoloaders and providers from the registry file(s)
- build a merged module relation registry
What ModuleRegistrar does
Inside vendor/schoolpalm/module-bridge/src/Providers/ModuleRegistrar.php the behavior is:
-
Register normal modules (
registerModules($registryPath, $context))- Loads registry JSON or PHP registry
- Normalizes entries to a flat list (SDK context is handled differently than production context)
- For each module:
- registers an autoload function for its namespace → base path
- registers all service providers found under
{$namespace}\\Providers
-
Register snapshots (
registerSnapshots($registryPath))- Similar to normal modules, but uses snapshot registry fields like
execution_path - Registers snapshot providers and autoloaders from the snapshot execution path
- Similar to normal modules, but uses snapshot registry fields like
-
Boot relations (
bootRelations($modulesRegistryPath, $snapshotsRegistryPath, $context))- Loads both module and snapshot registries
- Builds a merged relation registry using
RelationRegistryBuilder - Stores the merged registry in the container key
module.relations
How the SDK consumes the registered modules
At runtime, the SDK controllers rely on module/entry resolution via the SDK/container + module-bridge registries. Key usage points:
ModuleControllerusesModuleRegistryand aModuleResolverto locate module logic.SDKControllerusesCreatedRegistryand/orModuleResolverfor module entry execution.
Module lifecycle
1) Create module (manifest)
Command:
php artisan sp:make-m {name?}→MakeModuleCommand
Flow:
- Ask for module name (human readable) and choose a minimum role.
- Build
module_keyusing configuredvendorand a slug of the name. - Generate the initial
manifest.jsonusingManifestFactory::make($data). - Validate the manifest against the schema via
ManifestValidator::validate(...). - Create module directory under the configured module path.
- Write:
manifest.jsonpipeline.jsonfromstubs/dev-pipeline.json
- Register the module in the created registry via
CreatedRegistry::register($data).
ManifestValidator / schema validation
- schema file comes from
ModulePaths::schemaPath() - normalization occurs prior to validation to ensure missing defaults and object/array shapes align with schema expectations.
2) Scaffold module folder structure
Command:
php artisan sp:gen-s→GenerateModuleCommand
Flow:
- Select a module from the created registry (
chooseModule(), defined inModuleCommandBase). - Read
manifest.jsonfrom the module path. - Use
ModuleScaffoldto generate folders and starter files.
3) Validate module
Command:
php artisan sp:validate-m→ValidateModuleCommand
The validator enforces JSON schema correctness.
4) Install module (routes wiring)
Command:
php artisan sp:install→installModuleCommand
Current behavior in the repo:
- Calls
ModuleInstaller::make()->ensureInstallPaths()->installRoutes() - Copies vendor module routes into the application’s routes.
Note: The code you provided for
installModuleCommand.phponly shows route installation; other steps might be part ofModuleInstaller.
Runtime request flow (how actions run)
Route definition
The SDK registers a catch-all module route in:
src/routes/vendor_routes.php
Route::prefix('/')->group(function () { Route::match( ['get','post','patch','delete','put'], '{portal}/{module?}/{action?}/{id?}', [ModuleController::class, 'handle'] ); });
Route segments:
portal— tenant/portal/root context (e.g.admin,teacher,student)module— which module to executeaction— which action within the moduleid— optional record identifier
ModuleController: resolve → execute → return
In src/http/ModuleController.php:
-
Constructor
- Reads module segment from route using
Helper::getRouteSegment('module') - Normalizes it via
Helper::normalizeModuleName(...)
- Reads module segment from route using
-
resolveModule() (lazy)
- Builds a
contextarray from route segments:portal,moduleName,action,id
- Creates a
BaseModule($context) - Injects a
ModuleResolverbuilt fromModuleRegistry::class->all() - Stores the module instance for reuse
- Builds a
-
handle(Request $request)
- If module is missing: calls
dashboard() - Otherwise:
$module->performAction()- if AJAX or JSON requested, returns JSON; else returns the normal response
- If module is missing: calls
-
dashboard()
- Resolves module (or creates a temporary
BaseModule) - Uses Inertia to render the dashboard Vue component:
$module->getResolver()->resolveDashboard()
- Resolves module (or creates a temporary
Action dispatch (mapping action → method)
Action dispatch is implemented in src/core/ActionResolver.php.
Key concepts:
portal,moduleName,action,idare derived from the route segments in the constructor.rootDir = app_path('SDK')loadActions()loads PHP classes from anActionsdirectory
action → runX()
getMethodName($prefix = 'run') transforms the action string.
Example:
- action:
view-list→ method:runViewList - action:
add-student→ method:runAddStudent
handleActions()
handleActions():
- Computes
$method = $this->getMethodName() - If the method exists on the current object, it is called.
- Otherwise it iterates over loaded action classes in
$this->actions. - If still not found, aborts with
403: No action defined.
Delegation to Actions classes
loadActions():
- sets
$actionPath = $this->rootDir . '/Actions' - uses
$actionNamespace = 'App\\SDK\\Actions' - instantiates each action class found in that directory
__call($method, $args) also delegates to these action instances.
SDKController: module host and module API entry
src/http/SDKController.php provides additional endpoints:
handle($module, $action = null)
This route is separate from the dynamic {portal}/{module}/{action}/{id} route.
Main steps:
- Uses
CreatedRegistry::get($moduleKey)to find an installed/generated module. - Verifies a built JS bundle exists under:
public/build/modules/{root}/{module}.js
- Returns a view called
shellwith module and port metadata.
moduleHost($context, $module, $action = null)
Builds an Inertia render payload (ModuleHost) with a module URL.
- In local dev: points to
http://localhost:{port}/index.html - In non-local: points to an internal route
/modules/{context}/{module}/{action}
moduleApi()
Executes a module’s main entrypoint class:
- Extracts module path segment with
Helper::getPathSegment('module') - Checks module exists in
CreatedRegistry - Resolves the module main class using
ModuleResolver - Creates an instance and calls:
$entry->init($resolver->module)$entry->performAction()
Manifests: how metadata affects runtime
The manifest is created/normalized via:
ManifestFactory::make($data)ManifestValidator::validate($manifest, ...)
Important manifest fields for understanding behavior:
module_key— unique identifier used by registriesnamespace— base namespace for autoloading and provider discoverylevel+is_common— affects namespace/layout (multi-level modules)entry.provider— provider class used as module entrymenus— default menu entries for UI integrationactions— declared actions (scaffolding may use this)dependencies— backend/frontend dependenciesmigrationsandmodels— used by installers/scaffolders
The SDK also generates default events/listeners and default provides/contracts if not provided.
Configuration
The SDK publishes config/sdk.php to the host app.
ModuleSDKServiceProvider:
- merges
ModulePaths::configPath()into Laravel config keysdk - merges
BridgeHelper::configPath()intosdkas well - merges snapshot/module registrations
Key config in config/sdk.php includes:
modules_pathinstall_pathvendornamespacedefaultsfor manifest generationmodules cache key
Console commands (quick reference)
From the service provider, these commands are registered:
sp:make-m→ create module manifest/directory and register itsp:validate-m→ validate manifest against JSON schemasp:gen-s→ scaffold module structuresp:stuband related commands → generate stubs/templatessp:install→ install module routessp:install+ should/should-run variants → (additional lifecycle)
Purpose statement (short)
Purpose of the SchoolPalm Module SDK: provide an opinionated framework to build SchoolPalm modules—manifest-first creation, schema validation, scaffolding, route installation, and runtime action execution.
Purpose of the integration with module-bridge: ensure modules are discoverable and loadable at runtime by registering namespaces/autoloaders/providers from registries and exposing a relations registry used by module resolution.
Notes / terminology consistency
portal: current portal/root context from routesmodule/moduleName: requested module key/name from routesaction: requested action slug from routes (mapped torun{Studly(action)})id: optional entity identifierActions/directory: where module action classes are loaded from
Files referenced (for quick navigation)
src/ModuleSDKServiceProvider.phpsrc/routes/vendor_routes.phpsrc/http/ModuleController.phpsrc/http/SDKController.phpsrc/core/ActionResolver.phpsrc/Manifest/ManifestFactory.phpsrc/Manifest/ManifestValidator.phpvendor/schoolpalm/module-bridge/src/Providers/ModuleRegistrar.php