Generating demo data in Loader class using Loader::generateDemoData()
Purpose
Demo data shows what the app can do right after installation. It is used for presentations, trials, screenshots, manual testing and development. generateDemoData() creates such sample records for your app.
The default implementation in Hubleto\Framework\App does nothing:
public function generateDemoData(): void
{
// to be overriden
}
When it runs
Demo data is generated by Hubleto\Erp\Cli\Agent\Project\GenerateDemoData. It runs at the end of php hubleto init when the init config contains generateDemoData: true (or you answer yes at the prompt).
The generator:
- temporarily grants all permissions (
DANGEROUS__grantAllPermissions()), - updates the default company and creates demo users with roles (chief officer, manager, employee, assistant, external),
- creates the core CRM data (customers, contacts, activities, leads, deals), if these apps are installed,
- calls
generateDemoData()of every installed app, - revokes the permissions again.
From Hubleto\Erp\Cli\Agent\Project\GenerateDemoData::run() (part)
foreach ($this->appManager()->getInstalledAppNamespaces() as $appNamespace => $appConfig) {
$app = $this->appManager()->getApp($appNamespace);
if ($app) {
$app->generateDemoData();
}
}
$permissionsManager->revokeGrantAllPermissions();
Because step 3 runs first, your generateDemoData() can use customers, contacts, users and deals that already exist.
NOTE
help.mddoesn't list it, but the CLI dispatcher also hasphp hubleto project generate-demo-data. The generator assumes that it runs right afterinit(it uses company #1), so on an existing database it may create duplicates.
Examples
Minimal
apps/Suppliers/Loader.php
public function generateDemoData(): void
{
$mSupplier = $this->getModel(Models\Supplier::class);
$mSupplier->record->recordCreate(['name' => 'ElectroCorp']);
}
Records linked to existing data
The Orders app creates a document template and nine orders for random existing customers and states:
apps/Orders/Loader.php (shortened)
public function generateDemoData(): void
{
$mCustomer = $this->getModel(\Hubleto\App\Community\Customers\Models\Customer::class);
$customerCount = $mCustomer->record->count();
$mState = $this->getModel(Models\State::class);
$stateCount = $mState->record->count();
$mOrder = $this->getModel(Models\Order::class);
$mHistory = $this->getModel(Models\History::class);
for ($i = 1; $i <= 9; $i++) {
$idOrder = $mOrder->record->recordCreate([
'id_customer' => rand(1, $customerCount),
'id_state' => rand(1, $stateCount),
'identifier' => 'O' . date('Y') . '-00' . $i,
'title' => 'This is a test bid #' . $i,
'price' => rand(1000, 2000) / rand(3, 5),
'id_currency' => 1,
'date_order' => date('Y-m-d', strtotime('-' . rand(0, 10) . ' days')),
])['id'];
$mHistory->record->recordCreate([
'id_order' => $idOrder,
'short_description' => $this->translate('Order created'),
'date_time' => date('Y-m-d H:i:s'),
]);
}
}
TIP
rand(1, $customerCount)assumes that IDs are continuous. It works right afterinit. For robust code, load real IDs:$customerIds = $mCustomer->record->pluck('id')->toArray();and pick from them.
Idempotent demo data
The HrLeave app checks whether a record exists before creating it, so running the generator twice doesn't create duplicates:
apps/HrLeave/Loader.php (shortened)
public function generateDemoData(): void
{
$mUser = $this->getModel(\Hubleto\App\Community\Auth\Models\User::class);
$user = $mUser->record->where('is_active', true)->orderBy('id')->first();
if (!$user) return;
$mLeaveType = $this->getModel(Models\LeaveType::class);
$leaveTypes = [];
foreach ([
['name' => $this->translate('Annual leave'), 'days' => 25, 'paid' => 1],
['name' => $this->translate('Sick leave'), 'days' => 10, 'paid' => 1],
['name' => $this->translate('Personal leave'), 'days' => 3, 'paid' => 0],
] as $typeData) {
$type = $mLeaveType->record->where('name', $typeData['name'])->first();
if (!$type) {
$created = $mLeaveType->record->recordCreate([
'name' => $typeData['name'],
'annual_entitlement' => $typeData['days'],
'is_paid' => $typeData['paid'],
'requires_approval' => 1,
'description' => $this->translate('Demo leave policy.'),
]);
$type = $mLeaveType->record->find($created['id']);
}
$leaveTypes[] = $type;
}
$mRequest = $this->getModel(Models\LeaveRequest::class);
$pendingType = $leaveTypes[0];
$pendingRequestExists = $mRequest->record
->where('id_user', $user->id)
->where('id_leave_type', $pendingType->id)
->where('reason', 'Demo pending leave request')
->exists();
if (!$pendingRequestExists) {
$mRequest->record->recordCreate([
'id_user' => $user->id,
'id_leave_type' => $pendingType->id,
'date_from' => date('Y-m-d', strtotime('+14 days')),
'date_to' => date('Y-m-d', strtotime('+16 days')),
'balance_year' => (int) date('Y'),
'days_requested' => 3,
'id_approver' => $user->id,
'reason' => 'Demo pending leave request',
]);
}
}
Hierarchical lists
The Products app creates a category tree with fixed IDs, so child categories can reference their parents:
apps/Products/Loader.php (part)
$categories = [
[ 'id' => 100, 'id_parent' => 0, 'name' => $this->translate('Computers & Accessories') ],
[ 'id' => 110, 'id_parent' => 100, 'name' => $this->translate('Laptops & Desktops') ],
[ 'id' => 111, 'id_parent' => 110, 'name' => $this->translate('Laptop Computers') ],
[ 'id' => 200, 'id_parent' => 0, 'name' => $this->translate('Mobile Devices & Communication') ],
// ...
];
Rules
- Use
recordCreate(), not raw Eloquentcreate(). Your model callbacks run (identifiers, history, default workflow), and the demo data looks like real data. - Create realistic data. Relative dates (
strtotime('+14 days')) keep the demo fresh whenever it is generated. - Cover the features of your app: different states, workflow steps, overdue items. The badges, alerts and dashboards should show something.
- Be defensive. Return early when required data is missing (
if (!$user) return;). Check other apps with$this->appManager()->isAppInstalled(...). - Translate texts that users see (names of types, categories). Keep technical markers (like
'Demo pending leave request'used for the existence check) untranslated. - Don't put required data here. Default lists that the app needs to work belong to
installApp().
Demo data generated by the CLI template
php hubleto create app generates an empty method:
src/apps/MyFirstApp/Loader.php (generated)
// generateDemoData
public function generateDemoData(): void
{
// Create any demo data to promote your app.
}
A complete example for the Book model created by php hubleto create model MyFirstApp Book:
public function generateDemoData(): void
{
$mBook = $this->getModel(Models\Book::class);
$demoBooks = [
['varchar_example' => 'The Pragmatic Programmer', 'text_example' => 'Hunt, Thomas', 'decimal_example' => 39.90],
['varchar_example' => 'Clean Code', 'text_example' => 'Robert C. Martin', 'decimal_example' => 35.50],
['varchar_example' => 'Domain-Driven Design', 'text_example' => 'Eric Evans', 'decimal_example' => 54.00],
];
foreach ($demoBooks as $book) {
$mBook->record->recordCreate($book);
}
}
