Since the GenHelm Runtime does not require a database, sometimes it makes sense to build small sites using text files rather than introducing a database dependency. GenHelm includes a number of models and runtime features that make it easy to work with text files. Let's review some of these in this help file.

Storing Text Files

Normally, text files are stored within your site's private_data/[site]/data folder. If you are storing a lot of text files you would usually organize these under subfolders of the data folder. Let's look at some ways that such files might be stored.

Defining Tab-Delimited Files using the Report Model

The report model help explains how you can define tab-delimited files that get automatically saved to the data folder when the user completes a transaction. The names of the files can include dollar functions so that you can dynamically assign file and subfolder names based on things like session variables and dates.

Creating a Tab-Delimited File API

One of the framework base classes that you can extend is named tabbed_text_api. This supplies some methods that you can use to save and retrieve text file data. Here we see an example of a class used to store payments:

Sample payment_api class

This class just needs to define the data that will be used by the base class to manage the updating of a text file to keep track of all of the payments.

Configuring the Columns

The following static properties need to be defined:

entity_name

This entity name will be used in messages generated by the base class.

column_names

List the column names to be stored in the text file. This is akin to defining fields within your "database".

key_fields

Identify the columns that make up a unique key to be used to access rows in the file.

get_filename function

Next you need to define a function named get_filename that returns the name of the file to be used. This file will be stored within the data directory of the site by default. To specify an alternate folder you can code a function named get_folder_identifier which returns a folder identifier. You can also supply a function named get_subfolder which can return the name of a subfolder in which to store the data file.

Storing Records

Here we show an example of a function that uses the payment API to store a payment.

function append_to_payment_log($invoice_number,$license_key,$named_developers,$payment) {
    // Payments will start at 100001 and increment by 1
	$rec['id'] = $this->site->dollar_function('nextnum','next_payment_number',100001,1);
	$rec['date'] = $this->site->dollar_function('date','','','yyyymmdd');
	$rec['time'] = $this->site->dollar_function('time','','','hh:mm');
	$rec['ip'] = $_SERVER['REMOTE_ADDR'];
	$rec['invoice_number'] = $invoice_number;
	$rec['email'] = $this->email;
	$rec['payment'] = $payment;
	$rec['currency'] = $this->currency;
	$rec['payment_method'] = $this->payment_method;
	$rec['fees'] = $this->fees;
	$rec['licenses'] = $named_developers;
	$rec['license_key'] = $license_key;
	require_once SITE_CLASS_PATH.'payment_api.php';
	$api = new payment_api($this->site);
	if (!$api->save_record($rec)) {
		trigger_error('Error saving payment record '.var_export($rec,false).' Error returned: '.
                      $api->obtain_message(true,true),E_USER_WARNING);
		return false;
	}
	return true;
}

Retrieving Records

This next sample shows code that might be placed within a form hander. Let's assume that the form contains a field named license_key. We use the payment API to find all the rows that have a license key that matches the one entered by the users and it checks the date on the newest matching record to see if the key was generated within the past year.

function post() {
  $this->license_key = $this->field_list['license_key']->value();
  require_once SITE_CLASS_PATH.'payment_api.php';
  $payment_api = new payment_api($this->site);
  // Get a count of the records that have this licence key
  $found = $payment_api->find_records(array('license_key' => $this->license_key));
  if (empty($found)) {
    $this->form->assign_message('admin',6001,$this->license_key,
                 'The license key you have entered (:1:) does not seem to be valid.',
                  __LINE__.' '.__CLASS__);
    return false;
  }
  // Get the list of records and check the date of the newest one
  $records = $payment_api->get_found();
  foreach ($records as $record) {
    $newest = $record['date']; 
  }
  $oldest_allowed = $this->site->dollar_function('date','','-366 days',array('yyyymmdd'));
  if ($newest < $oldest_allowed) {
    $_SESSION['attempts']++;
    $this->form->assign_message('admin',6003,$this->license_key,
           'The license key you have entered (:1:) has expired.',
            __LINE__.' '.__CLASS__);
    return false;
  }
  return true;
}

Roll Your Own Handler

You can also just build your own handler in PHP and use the system output_file_class to save the file for you as shown here:

// Create a tab-delimited row
$quote = $code."\t".$desc."\t".$quantity."\t".$price."\t".$disc."\n";
// Instantiate the class to save a file
require_once CLASS_PATH.'output_file_class.php';
$quote_log = new \system\output_file_class($this->site);
// Set the name of the file (by default this is saved within the data folder)
$quote_file_name = 'quotes/'.$this->order_number.'.txt';
$quote_log->set_filename($quote_file_name);
// If the file already exists, append to it otherwise create it
$quote_log->append_or_create($quote);

This class can also be used to update files. You should only use this option for files that can be fully loaded into memory.  As you can see in the code sample above, when calling the update_file method you must supply an object instance and a method name. This method will be passed the original file contents and it must return the contents of the new file. You can optionally pass a parameter that will be forwarded to the supplied change method.

function change_email($from,$to) {
  $wf = $this->site->create_object('output_file_class');
  if ($wf->set_filename('sales.txt')) {
      $full_filename = $wf->get_full_filename();
      $email['from'] = $from;
      $email['to'] = $to;
      if ($wf->update_file($this,'change_email',$email)) {
          return $full_filename.' changed successfully';
      }
  }
  return $full_filename.' change failed';
}
function change_email($content,$email) {
	return str_replace($email['from'],$email['to'],$content);
}

The signature for the output_file_class' set_filename method is shown below.

function set_filename($filename,$dir='data',$subdir='',$public=false)

If you don't want to write to the site's data subdirectory you can pass an alternate identifier in parameter 2. Parameter 3 can be used to set a subfolder within the folder identified in parameter 2. The set_filename method will create the necessary folders if they don't exist. By default the folder(s) will be created under the site's private_data path. To create the folder under the site's public_html folder you can pass true in parameter 4. The first 3 parameters allow embedded dollar functions as we illustrate here:

$wf = $this->site->create_object('output_file_class');
if ($wf->set_filename('$time(,,His).txt','logs','$date(,,yyyymm)/$date(,,dd)')) {
	$full_filename = $wf->get_full_filename();
	return $full_filename;
}
return 'Failed to set file name';

This would return a file such as: 

C:/websites/private_data/somesite/logs/202205/19/144226.txt

Searching Text Files

Next let's look at how we could write a web page to search the payment text file. Before we do this, let's write a quick program to generate some test payments into our text file.

Generating Test Data

For this we will use the custom model. As explained in the custom model help, you must code one function to generate the output for the custom model. Let's take a look at this function:

// Allow number of records to be supplied in the query string, default to 100
$test_records = $this->site->parameters->get_or_default('rows',100,'integer');
$api = $this->site->create_object('payment_api');
// Define some valid values, we will pick these randomly
$currency = array('USD','CAD');
$method = array('cash','credit_card','check','wire');
$days_back = $test_records; // Make all the dates in the past
for ($i = 1; $i <= $test_records; $i++) {	
	$rec['id'] = $this->site->dollar_function('nextnum','next_payment_number',100001,1);
	// Add a day for each request so we have lots of dates in our test data, same for hours
	$rec['date'] = $this->site->dollar_function('date','','-'.$days_back--.' days','yyyy-mm-dd');
	$rec['time'] = $this->site->dollar_function('time','','+'.$i.' hours','hh:mm');
	$rec['ip'] = rand(1,999).'.'.rand(1,999).'.'.rand(1,999);
	$rec['invoice_number'] = $i + 200000;
	$rec['email'] = 'name'.rand(1,20).'@example.com';
	$rec['payment'] = round(rand(5000,50000)/97,2);
	$rec['currency'] = $currency[rand(0,1)];
	$rec['payment_method'] = $method[rand(0,3)];
	$rec['fees'] = rand(100,999)/100;
	$rec['licenses'] = rand(1,100);
	$rec['license_key'] = $this->site->dollar_function('random_str',20,25);
	if (!$api->save_record($rec)) {
		trigger_error('Error saving payment record '.var_export($rec,false).' Error returned: '.
                      $api->obtain_message(true,true),E_USER_WARNING);
		return false;
	}
}
return $test_records.' test records were created';

As you can see this page allows the user to pass in a number indicating how many records should be stored. It makes use of some dollar functions in order to help generate the random data values. After stowing this program on our local server as _create-test-payment-records, we can load the page using a url such as:

Executing our test data generator

Before continuing, let's digress briefly to explain why this program was named with a leading underscore character.  Since this page is only intended for use in the sandbox we could give it any name we want however we should delete the page once we are finished using it so that it does not accidentally get promoted to production. Recall that you can delete a component and all related generated components using the delete command, as in

delete _create-test-payment-records.

A more convenient notation when you want to delete something you are currently editing is delete =.

What if we decide to keep this page in case we want to generate more test records later on? Luckily GenHelm has a great feature to accommodate this while eliminating the risk of accidentally promoting the page to production. Recall that one of the common page properties is called Allow URL Access. One of the settings for this field is Sandbox Only as shown here:

Only allow page execution within the sandbox or development environment

Using this setting provides two important benefits:

  1. Sandbox Only pages are not included in the search results when searching for pages to be promoted or when comparing to production.
  2. Pages designated as Sandbox Only will not be executed within a production environment, even if they are somehow promoted.

We named the page beginning with an underscore to adhere to GenHelm's recommendations for naming Sandbox Only pages.

Now let's get back to our example. Here we show what the generated test data looks like in Excel:

Generated test data

Creating a Search Form

Next we will use the form model to build a form that will allow the user to perform payment searches. Before building this form let's discuss the fields that we will need for our form.

Date Ranges

Let's allow the user to enter a starting and ending date range to see payments within certain dates. We will begin by using the html_field_type model to define an intelligent date control that will allow any date within the past three years. Here we see how the $date function can be used for this:

Custom date type using html_field_type model

We stow this field type under the name past_3_years

Payment Type Selector

Next we will create a select field that will allow the user to choose certain types of payments. We use the select model to create the following specification which we stow as paid_by.

Payment type select control

Create any Predefined Fields

In addition to field types, the GenHelm framework allows the fields themselves to be predefined. Whether you decide to do so usually depends on whether you plan on using the fields on several forms. If you think this is likely, then it best to predefine them to ensure the fields are handled consistently across all of the forms that use the field. If you don't anticipate that the field will be needed on more than one form you can just define it on-the-fly when you add it to the form. Here we see the definition for the payment_type field. This illustrates how you could use shorter labels when the field is rendered on phones. We also choose the multiple option so that users can search for several payment types at once.

Sample html_field definition

The other fields will be defined directly on the form.

Payment Form

Let's start by placing the fields on the form. Field names are always preceded by :. :: can be used in the cell before a field to represent the label for the field.

Payment search form

Notice that the from_date and thru_date fields are based on the field type we created earlier. Payment method does not need any definition since this field was pre-defined. The email field is defined as part of system however the default behavior is for email to be required. In our case we want this to be optional so we need to override this on the form. Finally, we defined a search button to submit the form.

Notice that the search field has a green box around it. This is because we used <ctrl>+Enter to add field properties. In this case we added style="text-align:center".

Notice the last row contains ..result. The two dots are used to designate a placeholder field. We will use this cell as the container for our search results.

Since the last two cells should span all 4 columns we could include colspan within our property settings or we could just set the default setting for empty cell handling.

Colspan will be used to fill empty cells

We will stow this form using the name admin/search-payments since this will be a back-office function. Here we see what the form will look like:

First version of our search form

The fields are kind of crowded to let's add a little spacing using local styling.

Form local styling

The table which defines the form field is named according to the page name with slashes converted to two underscores.

Extend Payment API

The simple payment api we defined earlier does not have a way to search the payment file. Let's add this functionality next. Notice that we added four search fields to the property settings below.

Payment API with new search fields

We also added a simple Get method for the column_names to allow us to code the form handler without having to hard-code these names.

Since the user can select several payment methods let's add a custom set method for this field to allow for efficient searching.

Custom set method for the search_payment_method field

Notice that we have written the code so that the caller can pass in a single scalar payment method or an array of payment methods. In either case, the code has been written to convert these values into a keyed array in which the key value is the search method. This will make it easy to efficiently check whether our search results match what the user is interested in.

Finally we need to code the search method itself. Here we see the code for this.

function search() {
	$found = $this->find_records(); // Call base class to load all records
	if (!$found) {
		return false; // No records found
	}
	if (empty($this->search_date_from)) {
		// Default to only show previous year
		$from_date = $this->site->dollar_function('date','','-1 year','yyyy-mm-dd');
	}
	else {
		$from_date = $this->search_date_from;
	}
	$thru_date = empty($this->search_date_thru) ? '2999-12-31' : $this->search_date_thru;
	foreach ($this->found as $info) {
		if ($info['date'] < $from_date or $info['date'] > $thru_date) {
			continue; // Out of date range
		}
		if (!empty($this->search_email) and $this->search_email !== $info['email']) {
			continue; // Wrong email address
		}
		if (isset($this->search_payment_methods) and 
			!isset($this->search_payment_methods[$info['payment_method']])) {
			continue; // Wrong payment method
		}
		$match[] = $info;
	}
	if (isset($match)) {
		return $match;
	}
	else {
		$this->assign_message('misc',100,$found,':1: payments were searched; no matches were found',
							  __LINE__.' '.__CLASS__);
	}
}

Form Handler

Form handlers are used to add processing to a form. In this case we need to write some PHP code that will control the interaction between the form and the Payment API and return the search results to the form. Since we named our submit button search, if we call our form handler method search it will be called automatically when the button is clicked. Here we see the search method.

function search() {
	$api = $this->site->create_object('payment_api');
	// Copy the form fields to the payment_api search fields
	$api->set_search_date_from($this->field_list['from_date']->value());
	$api->set_search_date_thru($this->field_list['thru_date']->value());
	$api->set_search_payment_methods($this->field_list['payment_method']->value());
	$api->set_search_email($this->field_list['email']->value());
	// Perform the search
	$found = $api->search();
	if ($found) {
		// Build a table with the search results
		$column_names = $api->get_column_names();
		$thead = '<thead><tr><th>'.implode('</th><th>',$column_names).'</th></tr></thead>';
		foreach ($found as $info) {
			$td = '';
			foreach ($column_names as $name) {
				$td .= $this->html('td',$info[$name]);
			}
			$tr[] = $this->html('tr',$td);
		}
		$table = '<table class="bordered">'.$thead.$this->html('tbody',implode("\n",$tr)).'</table>';
		// Update the placeholder
		$this->set_placeholder('results',$table);
	}
	else {
		$api->transfer_your_messages_to_me($this->form);
	}
}

Since this is just an internal function we are just generating an HTML table to render this. We can use the base function called html to help build some of the tags. Notice that we have specified a bordered tag on the table. We could add this to the default stylesheet or we could add something like the following to the local styling in the form:

.bordered th, .bordered td {border: 1px solid; padding:2px}

The final step is to link the form hander to the form by adding the name of your form handler class as shown here:

Add a form handler to the form

Here we show the final search form showing our test data:

Search results screen

After you have tested this form you should change the login option since users should only have access to this form in production if they are logged into the system.

Restrict access to logged in users

You can also use the Function Access settings to restrict access to certain users.

Other "Database Like" Models

GenHelm supports several other models that could be useful when building sites that don't have access to or require a database. Generally speaking these are only suitable for "static" data or data that does not change very often since you will need to promote components to reflect changes in the data values.

datasheet

Can be used to define rows and columns of data much like a spreadsheet. Data can be retrieved using a simple API.

globals

Used to store data that can be looked up using the $global function.

glossary

This one is specific to creating a "database" containing definitions of terms.

messages

Used to store messages using sequential files.

php_array_data

Define rows and columns as class properties for quick access.

Using Databases

Now that we have showed how payments can be stored in text files, let's compare this to the process of storing and retrieving database table data.

Ecommerce Help Index

E-Commerce Overview Features and components used to build an online store.
Cart Items Defining products and services.
Shopping Cart Interacting with a shopping cart.
Working with Text Files How to store and process transactional data using text files.
Working with Databases Saving and retrieving database table data.
Transaction Numbers Generating identifiers for invoices and other transactions.
Taxes and Fees Configuring sales taxes and other cart fees.
Saving Customer Information Reading and writing customer information.
Accounting Data Managing account records.
Collecting Payments Processing credits cards as order payments.