www.hubleto.eu Reddit User Guide Stack Overflow LinkedIn Community Report an issue

Using php hubleto create mvc

Purpose

php hubleto create mvc generates everything needed to browse and edit the records of a model: a React table, a React form, a controller, a Twig view, a route, the component registration and a button on the app's home page.

php hubleto create mvc MyFirstApp Book

The command is implemented by Hubleto\Erp\Cli\Agent\Create\TableFormViewAndController.

Syntax

php hubleto create mvc <appNamespace> <model> [force] [noPrompt]
Argument Description
appNamespace The app, e.g. MyFirstApp. The app must be installed.
model An existing model of the app, e.g. Book.
force Any non-empty value: overwrite existing files.
noPrompt Any non-empty value: reinstall the app without asking.

Arguments.

Output

Real output:

$ php hubleto create mvc MyFirstApp Book
Hubleto CLI agent (release [dev-main]).
Code inserted into '/var/www/html/hubleto/src/apps/MyFirstApp/Loader.tsx' under '//@hubleto-cli:imports'.
Code inserted into '/var/www/html/hubleto/src/apps/MyFirstApp/Loader.tsx' under '//@hubleto-cli:register-components'.
Code inserted into '/var/www/html/hubleto/src/apps/MyFirstApp/Loader.php' under '//@hubleto-cli:routes'.
Code inserted into '/var/www/html/hubleto/src/apps/MyFirstApp/Views/Home.twig' under '{# @hubleto-cli:buttons #}'.

Table, form, view and controller for model 'Book' in 'Hubleto\App\Custom\MyFirstApp' created successfully.
Do you want to re-install the app?:   -> no

💡  TIPS:
💡  -> Install Hubleto's React UI framework.
💡  ->   npm install @hubleto/react-ui
💡  -> Compile Javascript and CSS into assets.
💡  ->   npm run build

Generated and modified files

src/apps/MyFirstApp/
├─ Components/
│  ├─ FormBook.tsx        # new: form (class component)
│  └─ TableBooks.tsx      # new: table (class component)
├─ Controllers/
│  └─ Books.php           # new: controller
├─ Views/
│  ├─ Books.twig          # new: view with the hblreact tag
│  └─ Home.twig           # modified: "Books" button added
├─ Loader.php             # modified: route added
└─ Loader.tsx             # modified: import and registration added

Names derived from the model Book:

Item Name
Controller and view Books (plural)
Table component TableBooks
Form component FormBook
Registered component MyFirstAppTableBooks
HTML tag <hblreact-my-first-app-table-books>
URL myfirstapp/books

Derived names.

Code inserted into existing files

Loader.php
//@hubleto-cli:routes
$this->router()->get([ '/^myfirstapp\/books(\/(?<recordId>\d+))?\/?$/' => Controllers\Books::class ]);
Loader.tsx
//@hubleto-cli:imports
import TableBooks from './Components/TableBooks';

//@hubleto-cli:register-components
globalThis.hubleto.registerReactComponent('MyFirstAppTableBooks', TableBooks);
Views/Home.twig
{# @hubleto-cli:buttons #}
<a class='btn btn-large btn-square btn-transparent' href='myfirstapp/books'>
<span class='icon'><i class='fas fa-table'></i></span>
<span class='text'>Books</span>
</a>

Controllers/Books.php (shortened)

<?php

namespace Hubleto\App\Custom\MyFirstApp\Controllers;

class Books extends \Hubleto\Erp\Controller
{
  // Uncomment this if you want to make your controller publicly
  // available without need for authentication.
  // public bool $requiresAuthenticatedUser = false;

  public function prepareView(): void
  {
    parent::prepareView();

    $this->viewParams['now'] = date('Y-m-d H:i:s');
    $this->viewParams['randomNumber'] = rand(1, 1000);

    $this->setView('@Hubleto:App:Custom:MyFirstApp/Books.twig');
  }
}

Views/Books.twig

<h1 class="app-main-title">{{ translate('MyFirstApp') }} > {{ translate('Books') }}</h1>

<hblreact-my-first-app-table-books
  string:tag="table-books"
  int:record-id="{{ viewParams.recordId }}"
  string:view="{{ viewParams.view }}"
  json:filters='{{ viewParams.filters|json_encode }}'
></hblreact-my-first-app-table-books>

Components/TableBooks.tsx (shortened)

import React, { Component } from 'react'
import TableExtended, { TableExtendedProps, TableExtendedState } from '@hubleto/react-ui/components/cc/TableExtended';
import FormBook from './FormBook';

interface TableBooksProps extends TableExtendedProps { }
interface TableBooksState extends TableExtendedState { }

export default class TableBooks extends TableExtended<TableBooksProps, TableBooksState> {
  static defaultProps = {
    ...TableExtended.defaultProps,
    formUseModalSimple: true,
    model: 'Hubleto/App/Custom/MyFirstApp/Models/Book',
  }

  translationContext: string = 'Hubleto\\App\\Custom\\MyFirstApp';
  translationContextInner: string = 'Components\\TableBooks';

  getFormModalProps(): any {
    let params = super.getFormModalProps();
    params.type = 'right wide';
    return params;
  }

  setRecordFormUrl(id: number) {
    window.history.pushState({}, "", globalThis.hubleto.config.projectUrl + '/books//' + (id > 0 ? id : 'add'));
  }

  renderForm(): React.JSX.Element {
    let formProps = this.getFormProps();
    return <FormBook {...formProps}/>;
  }
}

Components/FormBook.tsx (shortened)

import FormExtended, { FormExtendedProps, FormExtendedState } from '@hubleto/react-ui/components/cc/FormExtended';

export default class FormBook<P, S> extends FormExtended<FormBookProps, FormBookState> {
  static defaultProps: any = {
    ...FormExtended.defaultProps,
    model: 'Hubleto/App/Custom/MyFirstApp/Models/Book'
  }

  translationContext: string = 'Hubleto\\App\\Custom\\MyFirstApp';
  translationContextInner: string = 'Components\\FormBook';

  getStateFromProps(props: FormBookProps) {
    return {
      ...super.getStateFromProps(props),
      tabs: [
        { uid: 'default', title: <b>{this.translate('Book')}</b> },
      ]
    };
  }

  getRecordFormUrl(): string {
    return 'books/' + (this.state.record.id > 0 ? this.state.record.id : 'add');
  }

  renderTitle(): React.JSX.Element {
    const R = this.state.record;
    return <>
      <small>{this.translate('Book')}</small>
      <h2>{R.id <= 0 ? this.translate('New') : R.id}</h2>
    </>;
  }
}

Build and open

The React components must be compiled before the page works:

npm run build

Then open https://your-hubleto/myfirstapp/books:

Table generated by php hubleto create mvc The generated table for the `Book` model. The columns *Varchar*, *Text* and *Number* come from the sample columns in `Models/Book.php`. The `books` table must exist, see [create model](create-model).

Recommended changes to the generated code

The templates use the older class components. The community apps use functional components (see React components). After generating, consider:

Change Why
Fix the URLs in setRecordFormUrl() ('/books//') and getRecordFormUrl() ('books/') to myfirstapp/books/ The generated URLs miss the app's slug. After opening a record, the browser URL is wrong.
Replace the route with $this->router()->crud('myfirstapp/books', Controllers\Books::class); Also supports myfirstapp/books/add.
Rewrite TableBooks.tsx and FormBook.tsx as functional components in Components/FC/ Same style as the community apps. refactoring-guide.md in hubleto/react-ui has templates.
Add string:fulltext-search, json:column-search and string:form-active-tab-uid to the view The table then keeps its search and the active tab in the URL, like the community apps.
Remove the sample viewParams (now, randomNumber) from the controller Not used by the view.
Translate the button text in Home.twig {{ translate('Books') }}.

Recommended changes.

The same table as a functional component (Components/FC/TableBooks.tsx)
import React from 'react'
import Translator from '@hubleto/react-ui/core/Translator';
import Table from '@hubleto/react-ui/components/fc/Table';
import { TableMeta, TableProps } from '@hubleto/react-ui/components/fc/TableInterfaces';
import FormBook from './FormBook';

interface TableBooksProps extends TableProps {}

const componentName = 'TableBooks'; // must be the same as the exported const
const parentApp = 'Hubleto/App/Custom/MyFirstApp';
const T = new Translator(parentApp + '/Loader', 'Components/' + componentName);

const TableBooks = (props: TableBooksProps) => {
  return <Table
    componentName={componentName}
    parentApp={parentApp}
    model={parentApp + '/Models/Book'}
    baseUrlSlug='myfirstapp/books'
    formModalProps={{type: 'right wide'}}
    renderForm={(table: TableMeta): React.JSX.Element => {
      return <FormBook {...table.getDefaultFormProps()}/>;
    }}
    {...props}
  ></Table>
}

export default TableBooks;
The same form as a functional component (Components/FC/FormBook.tsx)
import React from 'react';
import Translator from '@hubleto/react-ui/core/Translator';
import { FormProps } from '@hubleto/react-ui/components/fc/FormInterfaces';
import Form from '@hubleto/react-ui/components/fc/Form';
import Input from '@hubleto/react-ui/components/fc/FormComponents/Input';

export interface FormBookProps extends FormProps {}

const componentName = 'FormBook'; // must be the same as the exported const
const parentApp = 'Hubleto/App/Custom/MyFirstApp';
const T = new Translator(parentApp + '/Loader', 'Components/' + componentName);

const TabDefault = (props: FormBookProps) => {
  return <>
    <Input field='varchar_example' />
    <Input field='text_example' />
    <Input field='decimal_example' />
  </>;
}

const FormBook = (props: FormBookProps) => {
  return <Form
    componentName={componentName}
    parentApp={parentApp}
    model={parentApp + '/Models/Book'}
    urlSlug='myfirstapp/books'
    title={{field: 'varchar_example', sub: T.translate('Book')}}
    tabs={{default: {content: () => <TabDefault {...props} />}}}
    {...props}
  ></Form>;
}

export default FormBook;

Update the import in Loader.tsx to ./Components/FC/TableBooks and rebuild.