Topincs

Programming

Topincs programming creates artifacts which uses very little code to achieve their task. This guide explains the fundamental concepts:

  • Tobjects (short for topic objects) provide read-write access to topics from PHP.
  • Domain classes extend the data-driven interface of tobjects with computational methods.
  • Services are parameterized scripts that can be called from the start page or topic pages.
  • Triggers are executed automatically when certain events in the web database happen.
  • Topic access filters are used to specify conditions under which a user group is denied access to certain topics.
  • 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.php

    Tobjects

    API reference

    Tobject

    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 nameItem typeMethod name examples
    invoice-numberName typehas_invoice_number
    get_invoice_number
    startOccurrence typeget_start
    set_start
    issueRole typeget_all_issues
    delete_all_issues
    issueTopic typemake_Issue
    isa_Issue
    invoiceTopic typemake_Invoice
    isa_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.

    stores/invoicing/php/domain/Invoice.php
    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");
    stores/invoicing/php/domain/Issue.php
    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");
    stores/invoicing/php/domain/WorkSession.php
    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 GET or POST,
    • 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

    create-service

    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

    ServiceParameters

    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.

    stores/invoicing/php/services/invoice/GET.php
    require_once("domain/Invoice.php"); // We use "invoice" as the formal name of our service parameter $invoice = $p->get("invoice");
    stores/invoicing/php/services/invoice/GET.xml
    <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:

  • after an occurrence of type end is inserted or updated
  • before a topic of type invoice is deleted
  • after a role of type invoiced is created
  • Admin reference

    create-trigger

    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
    Nameid of the parent
    Occurrenceid of the parent
    Roleid of the player
    Topicid 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:

    stores/invoicing/php/triggers/567.php
    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

    create-access-filter

    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.

    stores/invoicing/php/access/444.php

    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.

    Further readings