Programming
Topincs programming creates artifacts which uses very little code to achieve their task. This guide explains the fundamental concepts:
Example domain: Invoicing
Hint: Use Guest/Guest to log in to the example store accompanying this guide.
The concepts presented will be illustrated with code examples
from an invoicing
system of a software company: an invoice has an hourly rate
and invoices a number of issues for which a number of work
sessions have been used. A work session has a start and an end
of datatype xsd:dateTime. In this system we can
identify a need for:
- an XML view of an invoice for generating a PDF with XSLFO,
- a CSV view of all invoices for importing into another system,
- a service to access online banking to check which open invoices have been paid, and
- a service to compute and persist the time spent on every issue.
Where is the code?
Hint: The directory php is
initially not present. It is created automatically when
necessary.
In the directory TOPINCS_HOME/stores you find sub
directories for store specific code. In our example it looks
like this:
TOPINCS_HOME/stores/invoicing/php/
domain/
Invoice.php
Issue.php
WorkSession.php
services/
invoice/
GET.php
GET.xml
triggers/
567.phpTobjects
API reference
Hint: Both works: get_invoice_number or
getInvoiceNumber!
A service gains access to data in the store by retrieving a tobject. A tobject is a topic as a PHP object with a virtual interface driven by the constraints of its topic type. The following example fetches an invoice and prints its number and issues.
require_once("api/Tobject.php");
$invoice = Tobject::get($some_invoice_id);
echo $invoice->get_invoice_number() ."\n";
foreach ($invoice->get_all_issues() as $issue) {
echo $issue->label() ."\n";
}
Virtual interface
Hint: Keeping serialization names constant is a good idea, but a broken interface can be repaired by using a domain class!
The virtual interface is driven by constraints and serialization names. We need to assign serialization names to all relevant items on the page Serialization names, before we can inspect the interface on the page Programming interface. Both pages are reachable from the admin page of a store. The interface of our invoicing example is available online. The following table illustrates the relationship between serialization names and method names.
| Serialization name | Item type | Method name examples |
|---|---|---|
invoice-number | Name type | has_invoice_numberget_invoice_number |
start | Occurrence type | get_startset_start |
issue | Role type | get_all_issuesdelete_all_issues |
issue | Topic type | make_Issueisa_Issue |
invoice | Topic type | make_Invoiceisa_Invoice |
The interface allows access and manipulation of statements
about the topic that a tobject represents. There is several
method families available: has,
count, get, set,
add, delete, and
isa. The programming interface page only displays
the methods that make sense given the constraints of the topic
type.
Method families by example
Hint: The individual statements are independent from each other.
// has
if ($invoice->has_invoice_date()) {
...
}
// count
echo $invoice->count_issues();
// get
$rate = $invoice->get_hourly_rate();
foreach ($issue->get_all_work_sessions() as $session) {
...
}
// add
$invoice->add_issue($issue);
$some_invoice->add_all_issues($another_invoice->get_all_issues());
// delete
$session->delete_end(); // delete one
$invoice->delete_all_issues();
// set (delete all and then add)
$session->set_end(new DateTime());
$some_invoice->set_all_issues($another_invoice->get_all_issues());
// isa
if ($something->isa_Invoice()) {
...
}
Topic creation and deletion
// Creating a topic
$session = Tobject::make_WorkSession()
->set_start(new DateTime())
->set_issue($some_issue);
// Deleting a topic
$session->delete();
// Deleting many topics
Tobject::delete($issue->get_all_work_sessions());
Accessing instances of a topic type
// Iterate over all invoices - topic type 'Invoice' has the id 368
foreach (Tobject::get("id:368")->get_all_instances() as $invoice) {
...
}
Domain Classes
Hint: It is good programming practise to have only one class per file!
A domain class extends a tobject with non-data based methods. It can be registered for one or more topic types. The tobject will have methods of all domain classes that are registered (loaded) at the retrieval time for the type of its corresponding topic. It is convenient to think of a domain class to be assigned to a topic type, but this correspondence is not essential to a domain class. A domain class is rather a mixin which has certain implicit expectations towards the tobject.
Creating a domain class
A domain class must be created on the command line before it can be edited. A class name and the id of the topic type must be provided:
~ # cd /usr/local/topincs/stores/invoicing/
/usr/local/topincs/stores/invoicing/ # topincs create-class Invoice 368
/usr/local/topincs/stores/invoicing/php/domain/Invoice.php written.
In our invoicing system we need to calculate the time spent on
all issues of an invoice so we know how much to charge. This
information needs to be computed since it is not persisted in
the store. In order to distinguish computational results from
persistence, it is helpful to name methods of a domain class
other than get_something,
e.g. compute_something.
require_once("api/Tobject.php");
require_once("domain/Issue.php");
class Invoice extends Tobject {
function compute_amount() {
return $this->get_hourly_rate() * $this->compute_hours();
}
function compute_hours() {
$hours = 0;
foreach ($this->get_all_issues() as $issue) {
$hours += $issue->compute_duration_in_hours();
}
return $hours;
}
}
Tobject::register("Invoice", "id:368");
require_once("api/Tobject.php");
require_once("domain/WorkSession.php");
class Issue extends Tobject {
function compute_duration_in_hours() {
$duration = 0;
foreach ($this->get_all_work_sessions() as $session) {
$duration += $session->compute_duration_in_sec() / 60 / 60;
}
return $duration;
}
}
Tobject::register("Issue", "si:http://www.topincs.com/trial/invoicing/376");
require_once("api/Tobject.php");
class WorkSession extends Tobject {
function compute_duration_in_sec() {
return $this->get_end()->format("U") - $this->get_start()->format("U");
}
}
Tobject::register("WorkSession", "si:http://www.topincs.com/trial/invoicing/392");
Topincs services
Hint: One service can support both verbs!
A Topincs service satisfies a domain specific need. It is
composed of a service description defined on the schema page and
PHP files located in a directory below
TOPINCS_HOME/stores/STORE_NAME/php/services. Some
basics of HTTP requests will help your understanding. HTTP
requests
- are addressed to an URL,
- have a verb, usually
GETorPOST, - are returned a response which may have a body, e.g. a HTML document, and
- may specify a preference for a certain response format.
When you provide a read-only view of data in the store, you
should use the GET verb, otherwise
POST.
Creating a service
Admin reference
A service is created on the command line with the command
create-service. This creates two files and a
service topic. The arguments of the service must be specified on
the page of the service topic. Here is an example:
~ # cd /usr/local/topincs/stores/invoicing/
/usr/local/topincs/stores/invoicing/ # topincs create-service invoice GET XML
/usr/local/topincs/stores/invoicing/php/services/invoice/GET.php written.
/usr/local/topincs/stores/invoicing/php/services/invoice/GET.xml written.
http://localhost/invoicing/566 created.
In this case two files are created, GET.php and
GET.xml. The first one is a pure PHP file without
any markup. The second one is a markup file using PHP for
outputting variable sections and should be kept as simple as
possible. In case of POST the second one may
contain a summary of the operations performed.
Service arguments
Hint: You can access parameters parsed or unparsed!
API reference
You need to create the service arguments at the topic page of your service. The URI of this page is printed when creating the service. There is two kinds of arguments: topic arguments and value arguments. For the first ones you need to specify one or more topic types of which the instances can be used as parameters. They will be rendered as a select box in the parameter query form. For the latter you need to specify a datatype.
On a service call the arguments are bound to parameter
values. The service can access parameters via their formal
name through the variable $p which is an
instance of ServiceParameters.
In our invoicing system the following example creates an XML format of an invoice which interfaces with another system that generates a PDF document with XSLFO.
require_once("domain/Invoice.php");
// We use "invoice" as the formal name of our service parameter
$invoice = $p->get("invoice");<invoice>
<number><?php echo $invoice->get_invoice_number() ?></number>
<date><?php echo $invoice->get_invoicing_date()->format("j.n.Y") ?></date>
<service>
<?php foreach ($invoice->get_all_issues() as $issue) { ?>
<issue>
<name><?php echo $issue->label(false) ?></name>
<duration-in-hours><?php echo $issue->compute_duration_in_hours() ?></duration-in-hours>
<amount><?php echo $issue->compute_duration_in_hours() * $invoice->get_hourly_rate() ?></amount>
</issue>
<?php } ?>
</service>
<total>
<hours><?php echo $invoice->compute_hours() ?></hours>
<amount><?php echo $invoice->compute_amount() ?></amount>
</total>
</invoice>
Using a service
Services are available from the start page, where they will be accessed unparameterized. An auto-generated form will be presented to gather the service parameters, e.g. which invoice to generate. If the user is at the page of an invoice, he has the option to access the service parameterized.
Triggers
A trigger is a piece of PHP code that is executed when a certain event in the web database happens. Examples for such events are:
Admin reference
The trigger consists of a topic in the web database and a file in
the filesystem holding the PHP code. Both are created on the
command line by executing the Topincs command
create-trigger:
~ # cd /usr/local/topincs/stores/invoicing/
/usr/local/topincs/stores/invoicing/ # topincs create-trigger
/usr/local/topincs/stores/invoicing/php/triggers/567.php written.
http://localhost/invoicing/567 created.
The trigger topic is used to specify on which events the
trigger fires. There is event types for these constructs:
name, occurrence, role and topic. The PHP file
is passed a single parameter called $topic_id. This
table shows what this parameter is bound to when the trigger fires:
| Item type | $topic_id is bound to |
|---|---|
| Name | id of the parent |
| Occurrence | id of the parent |
| Role | id of the player |
| Topic | id of self |
Example
In our invoicing system work sessions are recorded as people progress through their issues. If the end of a work session is recorded, the persisted duration of the time spent on the issue should be updated:
require_once("api/Tobject.php");
require_once("domain/Issue.php");
// $topic_id is only parameter passed to a trigger!
$session = Tobject::get($topic_id);
$issue = $session->get_issue();
$duration_in_hours = $issue->compute_duration_in_hours();
if ($duration_in_hours) {
$issue->set_duration($duration_in_hours);
} else {
$issue->delete_duration();
}Now all that remains is to specify on the topic page of the trigger that it should run after an occurrence of type start or end is inserted, updated or deleted. With this mechanism the duration at the issue is always up to date.
Topic access filters
Admin reference
Hint: A filter is only file in the filesystem. It has no correspodance in the web database.
A topic access filter can be used to deny access to topics based
on a relationship between the topic and the user. To prevent
abuse in our invoicing system we want to restrict work sessions
to the person who is recorded to perform the work session (the
worker). First we need to create an access filter with
the Topincs command create-access-filter for a
certain user group which in our example has the id 444:
~ # cd /usr/local/topincs/stores/invoicing/
/usr/local/topincs/stores/invoicing/ # topincs create-access-filter 444
/usr/local/topincs/stores/invoicing/php/access/444.php written.
Then we need to specify the conditions in the newly created PHP
file which is passed all relevant information in the parameters
$topic_id, $user_id, and
$group_id.
Hint: The parameter $user_id represents the user account. You will need to connect it to something in your domain.
require_once("access/Access.php");
require_once("api/Tobject.php");
$topic = Tobject::get($topic_id);
$user = Tobject::get($user_id);
// $group = Tobject::get($group_id);
if ($topic::isa_WorkSession()) {
if ($topic->get_worker() != $user->get_person()) {
Access::deny();
}
}Summary
Topincs programming continues the Topincs principle to achieve a lot with little work. By assigning serialization names to statement types a virtual programming interface comes alive in a matter of minutes. Domain classes are a flexible tool to temporarily tie computational behavior to one or more topic types. They make it possible to change the underlying data in the topic map and still keep dependent code functional by using a domain class as an adaptor and importing it in the scripts that rely on the old data schema. These properties make it possible to quickly react to changes in the requirements.
