On this page

Last Updated: 1758255581

How to build a Standard App

SiteGUI's Standard App is a standard PHP class with a namespace, the class should contain some configuration settings for the app and several methods for handling app records. Methods should always return an array containing a key name "result" with the value "success" to indicate successful execution. 

Standard App must be submitted to SiteGUI Appstore for reviewing before it can be deployed. Once your app has been developed, simply use the  button next to your app name to submit it to the Appstore.

App Settings and Per Site Settings

App Settings (visibility, category) can be defined in a static method named config(), these values will determine whether the app is included in the app menu or not and whether the app records are accessible by different user groups. App can also create Per Site Settings by adding custom fields to allow Site Managers to input their own configuration values for the app. To create a custom field, just define field id (name), type, display name (label) and description for it. You can also set field visibility (visibility), a default value (value), whether the field is required or optional (is => required/optional/multiple) or accepting multiple values (select and lookup field only), and specify options if field supports them. The following field types are supported:

  • text/textarea: text input field
  • lookup: a lookup field that returns the IDs of matching object name (e.g: a Page name, a Product name, a User name). Supported objects are: creator (user), admin (staff), group (role), page, collection, menu_id, product, variant, sku, app name (e.g: feature_request).
  • select/radio: a field that consists several options to choose. Options can be pre-defined or can be looked up by specifying From::lookup as the first option, object (creator, page, permission) as the second option and optionally the value to lookup as the third option.  
  • file/image: a file/image field that allows uploading of file/image or selecting a file/image from File Manager
  • checkbox: a checkbox field
  • radio hover: a radio field that shows the selected or default option and upon mouse hovering, shows other available options to choose (e.g: an emoji selection)
  • rating: a clickable/slidable field that consists 5 stars to allow choosing a rate out of 5. Half-star or a rate of 4.5 is also supported.
  • password: a password field that can be used to store sensitive credentials, the value will be encrypted
  • url/email/tel/color: standard input fields
  • country: a dropdown field for selecting a country 

Below is an example of the static method config()

  public static function config($property = null) {
  $config['app_category'] = 'Bot';
  $config['app_permissions'] = [
     'staff_read' => 1,
     'staff_write' => 1,
     'staff_manage' => 1,
     'client_read' => 1,
     'client_write' => 1,
     'client_delete' => 0,
     'staff_read_permission' => '',
     'staff_write_permission' => '',
     'staff_manage_permission' => '',
     'client_read_permission' => '',
     'client_write_permission' => '',
     'client_delete_permission' => '',
  ];
  $config['app_configs'] = [
     'client_id' => [
        'label' => 'Client ID',
        'type' => 'text',
        'visibility' => 'editable',
        'size' => '6',
        'value' => '',
        'description' => 'Enter the Client ID for the App created at https://api.slack.com/apps',
     ],
     'client_secret' => [
        'label' => 'Client Secret',
        'type' => 'password',
        'visibility' => 'editable',
        'size' => '6',
        'value' => '',
        'description' => 'Enter the Client Secret provided by Slack',
     ],
     'oauth' => [
        'label' => 'Connect Slack',
        'type' => 'oauth',
        'value' => '',
        'options' => [
           0 => 'https://slack.com/oauth/v2/authorize?scope=chat%3Awrite%2Cchat%3Awrite.public%2Capp_mentions%3Aread%2Cchannels%3Aread%2Cchannels%3Ahistory%2Cgroups%3Ahistory%2Cim%3Ahistory%2Cmpim%3Ahistory&redirect_uri={redirect_uri}&state={state}&client_id={client_id}',
           1 => 'https://slack.com/api/oauth.v2.access',
           2 => 'client_id={client_id}&client_secret={client_secret}&code={code}',
           3 => '',
           4 => '',
           5 => '',
           6 => '',
        ],
        'description' => 'Authorize to retrieve access token automatically',
     ],       
     'access_token' => [
        'label' => 'Access Token',
        'type' => 'password',
        'visibility' => 'hidden',
        'size' => '6',
        'value' => '',
        'description' => 'Enter the Access Token provided by Slack',
     ],
     'refresh_token' => [
        'label' => 'Refresh Token',
        'type' => 'password',
        'visibility' => 'hidden',
        'size' => '6',
        'value' => '',
        'description' => 'Enter the Refresh Token provided by Slack',
     ],
     'bot_user_id' => [
        'label' => 'Bot User ID',
        'type' => 'text',
        'visibility' => 'hidden',
        'size' => '6',
        'value' => '{oauth::bot_user_id}',
        'description' => '',
     ],
     'callback' => [
         'label' => 'Webhook Listener',
         'type' => 'text',
         'description' => 'Enter any code to enable Webhook Listener for Slack',
         'value' => substr($_SESSION['token'], 8, 8),
     ],
     'fieldset1' => [
        'type' => 'fieldset',
        'fields' => [
           'channel' => [
              'label' => 'Channel',
              'visibility' => 'editable',
              'is' => 'optional',
              'type' => 'text',
              'value' => '',
           ],
           'ai_agent' => [
              'label' => 'Agent',
              'description' => 'Agent to react to events in the channel',
              'visibility' => 'editable',
              'is' => 'optional',
              'type' => 'lookup',
              'options' => [
                 'From::lookup' => 'From::lookup', 
                 'activation', 'Scope::Agent'
              ],
           ],
        ],    
     ],    
  ];
  $config['app_fields'] = [
     'title' => [
        'label' => 'Channel',
        'type' => 'text',
        'size' => '6',
        'value' => '',
        'description' => 'Enter the Slack public/private or IM channel',
     ],
     'content' => [
        'label' => 'Text Message',
        'type' => 'textarea',
        'size' => '6',
        'value' => '',
        'description' => '',
     ],
  ]; 
  $config['app_columns']['title'] = 'Channel';

  if ($property AND isset($config[ $property ]) ) {
     $response[ $property ] = $config[ $property ];
  } else {
     $response['config'] = $config;
  } 

  $response['result'] = 'success';
  return $response;
}            

Initialize the App

When the app is initialized, it should receive the configuration values for the running site and for the app itself (per site settings), these values are stored in $this->config (sample below). The View object is also available for the app to use at $this->view.

  {
  "site": {
    "id": 1000,
    "name": "SiteGUI",
    "url": "sitegui.com",
    "account_url": "my.sitegui.com",
    "template": "supersite",
    "status": "Active",
    "owner": 123456,
    "language": "en",
    "timezone": "America/New_York",
    "editor": "wysiwyg",
    "logo": "https://cdn.sitegui.com/public/templates/admin/bootstrap5/assets/img/logo.png",
    "locales": {
      "en": "english"
    }
  },
  "app": {
    "api_token": "xxxxxxx",
    "mode": "advance",
    "row_count": 25
  }
}  

SiteGUI ViewBlock

SiteGUI renders content using a Layout and ViewBlocks. Layouts are important for creating visually appealing and functional user interfaces. They allow developers to organize the components of an application in a logical and intuitive way. A layout is an arrangement of BlockHolders that can be displayed at a relative position of a view, such as main, top, bottom, left or right. SiteGUI supports the following BlockHolders by default:

  • block_head: should be attached to <head> tag, should contain non-visible content such as scripts or CSS (inline or URLs)
  • block_header: should be displayed at the header of a page (where logo and menu are displayed)
  • block_spotlight: should be displayed where attracts most attention from viewers
  • block_left: should be displayed to the left of the page
  • block_right: should be displayed to the right of the page
  • block_top: should be displayed at the top of the page
  • block_bottom: should be displayed at the bottom of the page
  • block_main: should be displayed as the main content. Page content is shown here
  • block_footnote: should be displayed near the end of a page but before the footer area
  • block_footer: should be displayed at the end of a page 
  • content_header: should be displayed at the header of the main content
  • content_spotlight: should be displayed where attracts most attention from viewers
  • content_left: should be displayed to the left of the main content
  • content_right: should be displayed to the right of the main content
  • content_top: should be displayed at the top of the main content
  • content_bottom: should be displayed at the bottom of the main content
  • content_footnote: should be displayed near the end of the main content but before the footer area
  • content_footer: should be displayed at the end of the main content

A layout may add more custom BlockHolders (should start with block_) but should support as many default BlockHolders as it could to make the layout usable with different applications which may attach ViewBlocks to those default BlockHolders. 

A ViewBlock is actually an array containing the following keys:

  • order (integer, optional): use for sorting if there are multiple ViewBlocks at one position
  • system (array): keys in this array will be converted to Smarty variables {$key}:
  • api (array): keys in this array will be appended to the Smarty variable {$api} and will be available via API access
  • html (array): keys in this array will be appended to the Smarty variable {$html} and will only be available via API access upon requested (html=1)
  • menu (array): an array of menu items which will be merged into a Smarty variable {$position_menu}  (position is the position to which the ViewBlock is attached e.g: {$top_menu})
  • output (HTML string): an HTML string which will be assigned to a Smarty variable {$block_name} (block_name is the name of the block i.e: block_main, block_top, block_1, block_2)
  • template (array): specify a Smarty template (to be rendered to HTML when "output" key is not defined) using the following keys 
    • string (base64 encoded string): a template string
    • file (file name without extension): a template file (e.g: page_edit)
    • directory (relative file path, optional): specify a directory relative to the base directory to load the template file specified above
    • layout (file name without extension, optional): specify a layout to use instead of the default layout, this is applicable to a ViewBlock attached to position "main" only.

    One or more ViewBlocks can be attached to a BlockHolder. When attaching to one of the default BlockHolders, just remove the prefix block_ and use the position directly (i.e: top, left or main instead of block_top, block_left or block_main). For custom BlockHolders, please use the full block name. Any Smarty variable assigned/appended in each ViewBlock are available for all other ViewBlocks to use (and may be overwritten by other ViewBlocks)Any ViewBlock attached to a BlockHolder will have their rendered HTML output appended to a Smarty variable {$block_name} (block_name is the name of the block i.e: block_main, block_top, block_1, block_2, content_top) and displayed at the specified position in the layout.

    Create a new or edit an existing record

    The method edit() and clientView() are called when the record editor is opened to create a new or edit an existing record by staff or client respectively. These methods should return an array of SiteGUI ViewBlocks attached to supported BlockHolders to alter the look and feel of the editor. The main block may specify a template name to be rendered or use the default one (app_edit.tpl). If the default template is used, the main block should indicate which default inputs should be shown and may add custom fields to the editor.

    Save a record

    The method update($page) and clientUpdate($page) are called whenever the app receives a submission from a staff or client respectively. These methods process the submitted data and then return them back to the main handler to store the data into the database. It is possible to bypass the saving of the data by returning a response with _abort set to true, in this case no record will be created/updated.

    Display a record on the website

    The method render() is called when a record is displayed. It receives the record data and returns an array of SiteGUI ViewBlocks that contain all variables and settings (layout, template name) to help the View object to render the record properly.

    Display a record category on the website

    The method renderCollection() is called when a record category is displayed. It receives the category data and returns an array of SiteGUI ViewBlocks that contain all variables and settings (layout, template name) to help the View object to render the category properly.

    Webhook Listener for App

    The method callback($data) is triggered when a callback from an external site is received. While this method does not have access to the database, it can use an AI agent object at $this->config['agentObj'] to interact with any available AI agent using justRun($agent, $input, $thread) method. This method should provide a response code/message to the caller, process the received request and optionally prepare the data to send to the App. The data to send to the App should include the record ID or slug if present, the slug should not be duplicated. The data will be processed asynchronously in the background under the site owner account. Please check the sample app below for more details.

    In order to enable the webhook listener, a config field 'callback' should be present, the value of this field will be appended to the webhook URL and served as the verifier code i.e: https://my.sitegui.com/account/cart/callback.json?app=App/Slack&verifier=215f0e99. App developer may use an optional, static method setCallback($config) to register the webhook URL with the external site if it supports setting up this URL programmatically. 

    Sample App

      <?php
    namespace SiteGUI\App;
     
    class Slack { 
       use \LiteGUI\Traits\Http;
       protected $config; 
       protected $view; 
    
       public function __construct($site_config, $view = null){
          $this->config = $site_config;
          $this->view	= $view;
       }
    
       public static function config($property = null) {
          $config['app_permissions'] = [
             'staff_write' => 1,
             'client_write' => 1,
          ];
          $config['app_configs'] = [
             'client_id' => [
                'label' => 'Client ID',
                'type' => 'text',
                'visibility' => 'editable',
                'size' => '6',
                'value' => '',
                'description' => 'Enter the Client ID for the App created at https://api.slack.com/apps',
             ],
             'client_secret' => [
                'label' => 'Client Secret',
                'type' => 'password',
                'visibility' => 'editable',
                'size' => '6',
                'value' => '',
                'description' => 'Enter the Client Secret provided by Slack',
             ],
             'oauth' => [
                'label' => 'Connect Slack',
                'type' => 'oauth',
                'value' => '',
                'options' => [
                   0 => 'https://slack.com/oauth/v2/authorize?scope=chat%3Awrite%2Cchat%3Awrite.public%2Capp_mentions%3Aread%2Cchannels%3Aread%2Cchannels%3Ahistory%2Cgroups%3Ahistory%2Cim%3Ahistory%2Cmpim%3Ahistory&redirect_uri={redirect_uri}&state={state}&client_id={client_id}',
                   1 => 'https://slack.com/api/oauth.v2.access',
                   2 => 'client_id={client_id}&client_secret={client_secret}&code={code}',
                   3 => '',
                   4 => '',
                   5 => '',
                   6 => '',
                ],
                'description' => 'Authorize to retrieve access token automatically',
             ],       
             'access_token' => [
                'label' => 'Access Token',
                'type' => 'password',
                'visibility' => 'hidden',
                'size' => '6',
                'value' => '',
                'description' => 'Enter the Access Token provided by Slack',
             ],
             'refresh_token' => [
                'label' => 'Refresh Token',
                'type' => 'password',
                'visibility' => 'hidden',
                'size' => '6',
                'value' => '',
                'description' => 'Enter the Refresh Token provided by Slack',
             ],
             'bot_user_id' => [
                'label' => 'Bot User ID',
                'type' => 'text',
                'visibility' => 'hidden',
                'size' => '6',
                'value' => '{oauth::bot_user_id}',
                'description' => '',
             ],
             'callback' => [
                 'label' => 'Webhook Listener',
                 'type' => 'text',
                 'description' => 'Enter any code to enable Webhook Listener for Slack',
                 'value' => substr($_SESSION['token'], mt_rand(0,24), 8),
             ],
             'fieldset1' => [
                'type' => 'fieldset',
                'fields' => [
                   'channel' => [
                      'label' => 'Channel',
                      'visibility' => 'editable',
                      'is' => 'optional',
                      'type' => 'text',
                      'value' => '',
                   ],
                   'ai_agent' => [
                      'label' => 'Agent',
                      'description' => 'Agent to react to events in the channel',
                      'visibility' => 'editable',
                      'is' => 'optional',
                      'type' => 'lookup',
                      'options' => [
                         'From::lookup' => 'From::lookup', 
                         'activation', 'Scope::Agent'
                      ],
                   ],
                ],    
             ],    
          ];
          $config['app_fields'] = [
             'title' => [
                'label' => 'Channel',
                'type' => 'text',
                'size' => '6',
                'value' => '',
                'description' => 'Enter the Slack public/private or IM channel',
             ],
             'content' => [
                'label' => 'Text Message',
                'type' => 'textarea',
                'size' => '6',
                'value' => '',
                'description' => '',
             ],
          ]; 
          $config['app_columns']['title'] = 'Channel';
    
          if ($property AND isset($config[ $property ]) ) {
             $response[ $property ] = $config[ $property ];
          } else {
             $response['config'] = $config;
          } 
    
          $response['result'] = 'success';
          return $response;
       }        
    
       public function edit($page) {	
          if ( !empty($page) ){
             $response['blocks']['main']['api']['page'] = $page;	
          }   
          $response['result'] = 'success';
          return $response;
       }
       public function clientView($page) { 
          return $this->edit($page);
       }
    
       public function update($page){
          $response['result'] = 'error';
          $params['channel'] = $page['title'][ $this->config['site']['language']??'en' ]??$page['title']??'';
          $params['text'] = strip_tags($page['content'][ $this->config['site']['language']??'en' ]??$page['content']??'', '<a>');
          if ($params['channel'] AND $params['text']){
             $slack_url = 'https://slack.com/api/';
             $headers[] = 'Content-Type:application/json; charset=utf-8';
             $headers[] = 'Authorization: Bearer '. $this->config['app']['access_token'];
    
             $slack = $this->httpPost($slack_url . 'chat.postMessage', json_encode($params), $headers);
             $slack = json_decode($slack, true);  
             if (!empty($slack['ok'])){
                $response['result'] = 'success';
                if (empty($page['id'])){
                   $slack2 = $this->httpGet($slack_url . 'conversations.info', $params, $headers);
                   $slack2 = json_decode($slack2, true);
                   if ( !empty($slack2['ok']) AND !empty($slack2['channel']['name']) ){
                      $page['name'] = $slack2['channel']['name'];
                   } else {
                      $page['name'] = $params['channel'];
                   }
                }
                unset($page['content']); //we dont keep content
                $response['page'] = $page;  
             } else {
                $response['message'] = 'Slack Error: '. $slack['error'];
                $response['_abort'] = 1; //abort, dont save to db
             }
          }   
          return $response;
       }
    
       public function clientUpdate($page){
          return $this->update($page);
       }   
    
       public function callback($data) {
          $data = json_decode($data??'', true);
          $response['status']['result'] = 'error'; //default  
    
          if (empty($this->config['app']['callback']) OR empty($_GET['verifier']) OR $this->config['app']['callback'] != $_GET['verifier'] ){
             $response['status']['code'] = 401;
             $response['status']['message'] = 'Unauthorized';
          } elseif ( array_key_exists('challenge', $data??[]) ){
             header('Content-Type: text/plain');
             echo $data['challenge'];
             exit;
          } elseif ( !empty($data['event']['text']??$data['event']['blocks']??$data['event']['attachments']) AND $data['event']['type'] == 'app_mention' ){
             foreach ($this->config['app']['fieldset1']??[] as $handler) {
                $handlers[ $handler['channel'] ] = current($handler['ai_agent']);
             }
             $agent = $handlers[ $data['event']['channel'] ]??
                $handlers['Default']?? //default
                $handlers['*']??null; //wildcard
                
             if ( !empty($this->config['agentObj']) AND $agent ){
                $slack_url = 'https://slack.com/api/';
                $headers[] = 'Content-Type:application/json; charset=utf-8';
                $headers[] = 'Authorization: Bearer '. $this->config['app']['access_token'];
                $params['channel'] = $data['event']['channel'];
                $params['latest'] = $data['event']['ts'];
                $params['limit'] = 10;
                $params['inclusive'] = true;
                $slack = $this->httpPost($slack_url . 'conversations.history', json_encode($params), $headers);
                $slack = json_decode($slack, true);
                if (!empty($slack['ok']) AND !empty($slack['messages'])){
                   $input = '';
                   foreach (array_reverse($slack['messages']) as $message) {
                      if (!empty($message['subtype']) AND 
                         in_array($message['subtype'], [
                            'bot_message',
                            'channel_join',
                            'channel_leave',
                            'channel_topic',
                            'channel_purpose',
                            'channel_name',
                            'message_deleted',
                            'channel_archive',
                            'channel_unarchive',
                            'group_join',
                            'group_leave',
                            'group_topic',
                            'group_purpose',
                            'group_name', 
                            'tombstone',
                         ])
                      ){
                         continue;
                      }   
                      $input .= $message['user'] .': '. ($message['text']?: json_encode($message['blocks']??$message['attachments']??'')) .'. ';
                   }
                } else {
                   $input = $data['event']['text']?: json_encode($data['event']['blocks']??$data['event']['attachments']??'');
                }
                if ($input){
                   $data['agent'] = $this->config['agentObj']->justRun($agent, $input, $data['event']['channel']);
                   if ( !empty($data['agent']['status']['result']) AND 
                      $data['agent']['status']['result'] == 'success' AND 
                      !empty($data['agent']['output']['content'])
                   ){
                      $content = $data['agent']['output']['content'];
                   } else {
                      $content = "I'm sorry. I couldn't process your query";
                   }
                } else {
                   $content = "I'm sorry. I couldn't process empty query";
                }
             } else {
                $content = "I'm sorry. The agent is on vacation and couldn't be located";
             }  
             if ( !empty($content) ){
                $this->update([
                   'title' => $data['event']['channel'],
                   'content' => $content,
                ]);
             }
             $response['status']['result'] = 'success';
          } else {
             $response['status']['result'] = 'success';
          }
          //set data to be sent to the App here
    	  //ID (if available) or slug will be used to look for the record to update it, if no record exists, new one will be created
    	  //$response['data']['id'] = 12345; 
    	  //$response['data']['slug'] = 'unique_slug';
    	  //$response['data']['name'] = 'Record name';
    	  //$response['data']['fields']['custom_field] = 'Custom';
    	  //$response['data']['sub']['Comment]['content']['en'] = 'A comment';
    
          return $response;
       }  
     
       public static function setCallback($config) {  
          if (!empty($config['bot_token']) AND !empty($config['callback_url']) ){ //callback_url is provided at runtime
             $slack_url = 'https://slack.com/api/';
             $headers[] = 'Content-Type:application/json; charset=utf-8';
             $headers[] = 'Authorization: Bearer '. $config['app']['access_token'];
             $params['url'] = $config['callback_url'];
             $params['secret_token'] = $config['callback'];
    
             $slack = self::httpPostStatic($slack_url . '/setWebhook', json_encode($params), $headers);
             $slack = json_decode($slack, true);
             if (!empty($slack['ok'])){
                return true;
             }   
          }   
          return false;
       }
    }