Developing ATK modules - Part 2 (Advanced)
Pizza Guides Index | Part 1 | Part 2 | Part 3
Introduction
So, you’ve read part 1 of the guide, perhaps even created some modules for Achievo, and you are ready to read about the more powerful features of the software.
In part 1, you’ve seen how you can build nodes, which represent management of information. In this guide, I will show you how these nodes can have relationships with other nodes. After that, I will explain ‘triggers’, functions that you can implement that are executed at certain events (for example after the insertion of a new record). Finally, I will explain some smaller topics that can make your application more user-friendly, like how to make your module support multiple languages. These three topics aren’t really related, but it’s better for you general understanding of the inner workings of Achievo, to discuss them in this order.
Prerequisites
Before starting to work with this guide, you should have at least read part 1. If you have a fair understanding of the things discussed there, and have toyed around with the example code, this guide should not be too hard to understand.
Some knowledge about relations in a database context (referential keys, etc.) is practical, but not absolutely necessary, as I will try to explain the relationships in very basic terms.
I will continue to use the example of the Pizza management application, so you should use the code from part 1 as a starting point for the examples in this guide. If you haven’t implemented the classes from the previous guide, check out its appendix, which contains the complete example code.
Relationships
Entities in a data model (tables) are seldom ‘stand alone’. Most of the time, they have some kind of relationship with surrounding entities. For example in Achievo, a project is related to a customer, a coordinator and to phases. A phase is related to activities, etc. Achievo’s backend contains features to quickly implement these relations in a user-friendly manner.
In the previous guide, you have seen that nodes contain attributes, that represent fields in the database. Just like attributes, a node can contain relations to other nodes. The addition of a relationship is just as easy as adding an attribute. There are several types of relations, each of which will be discussed in this chapter. The difference between the types of relations is made based on their ‘cardinality’ (for those of you unfamiliar with that term: cardinality indicates how many items can be involved in a relationship).
Many-to-one relation
Description
Of the three basic relation types, the many-to-one relation is probably the hardest to explain, but the easiest to implement, so I’ll start with this type of relation. Many-to-one means, as seen from our node, that many records of this type have a relationship with one record of some other node. As an example to explain this mumbo jumbo, we’re going to introduce ‘pizza categories’ and create a relationship between pizzas and categories.
Suppose we have a set of categories, (for example ‘small’, ‘medium’ and ‘large’). Suppose each pizza falls into one category. In one category, there can be many pizzas. So, from the pizza point of view, you might say there is a many-to-one (many-pizzas-in-one-category) relation with categories.
Pizza * <--------------> 1 Category
Such a many-to-one relation is represented in the pizza edit-screen by a dropdown box from which the user can select in which category the pizza belongs.
Ok, let’s implement this.
Preparations
First, we need a place to store categories. A category will only have a name, and of course, a primary key. This will lead to a simple table definition. For MySql, executing the following SQL statement on the Achievo database, should create the table for storing the categories:
<sql> CREATE TABLE pizza_categories (
id int(10) NOT NULL, name varchar(50) , PRIMARY KEY(id)
); </sql>
Then, we need to create a node in the pizza module. I’m not going to explain how to do this, as you already know everything you need to know to create such a node. But, just in case, here’s the basic implementation:
<?php
class pizza_category extends atkNode
{
function pizza_category()
{
$this->atkNode("pizza_category");
$this->add(new atkAttribute("id", AF_AUTOKEY));
$this->add(new atkAttribute("name", AF_OBLIGATORY|AF_UNIQUE|AF_SEARCHABLE));
$this->setTable("pizza_categories");
$this->setOrder("name");
}
}
?>
This should all look familiar to you. The only new thing you might have noticed is the use of the flag AF_AUTOKEY for the id attribute. In the previous guide, we used the flags AF_PRIMARY|AF_HIDE|AF_AUTO_INCREMENT. Actually, AF_AUTOKEY is a kind of 'shortcut flag', which does exactly the same as specifying the three flags separately. Saves you some typing on those primary key fields, which usually have these flags.
Save this code as class.pizza_category.inc in the pizza module directory. Next, we must add a menu-item to the Achievo menu, and setup access rights for the new node, by adding two lines to the appropriate functions in our module.inc:
In the getMenuItems() function, add this line:
$this->menuitem("pizza_categories", dispatch_url("pizzaman.pizza_category","admin"));
In the getNodes() function, add this line:
registerNode("pizzaman.pizza_category", array("admin", "add","edit", "delete"));
If these lines are not clear to you, read over the parts that deal with these issues in part 1 of this guide.
If you browse the Achievo installation now, you should be able to add some categories to the database. Add a few (‘Small’, ‘Medium’, ‘Large’ for example), we’ll use them later on.
There’s one preparation left to do, before we can implement the relation. In the pizza table, we need a new field to store the chosen category. Let’s call this field ‘category’. People might be in favor of calling such a referential key ‘category_id’, but I prefer to just name the field after the ‘role’ it performs. Use whatever you’re used to. The referential key must be of the same field type as the key it refers to (the id field in the pizza_categories table), so the following statement should alter the pizza table to add the correct field:
<sql> ALTER TABLE pizza ADD category int(10) NOT NULL; </sql>
Execute this statement on the Achievo database. We’re done with the preparations.
Implementing the relation
Implementing the relation is actually very simple. The previous paragraph was a bit lengthy, but that was because we first had to create something to create a relation to.
So we planned to implement a many-to-one relation in the pizza node, for which we added a ‘category’ field to the pizza table.
Relations are actually a special kind of attribute. They’re added to a node in exactly the same way as ‘regular’ attributes.
Open the class.pizza.inc file, and add the following line to the top of the file.
useattrib("atktextattribute");
useattrib("atknumberattribute");
useattrib("atkdateattribute");
userelation("atkmanytoonerelation");
Just like attributes, we have to specify this line so ATK can include the correct files. (Like in the previous guide, this line is only necessary if you run Achievo 0.9 or higher.)
Then, right after the attribute we added for the ‘entrydate’ field, we add a line to add the relation to the node:
...
$this->add(new atkNumberAttribute("price"));
$this->add(new atkDateAttribute("entrydate"));
$this->add(new atkManyToOneRelation("category", "pizzaman.pizza_category",
AF_OBLIGATORY|AF_SEARCHABLE));
The first parameter we pass to the atkManyToOneRelation is the name of the field that the relation is stored in. In our case, this is the ‘category’ field. The second parameter is the destination of the relation. We specify this as modulename.nodename, since a node with the same name might already exist in another module. Also, because this way you could create relations to nodes in other modules. (To create a relation to an existing Achievo base node, like ‘employee’, you don’t need to specify a module name, but we’ll talk about this later.) As third parameter, we specify that ‘category’ is an obligatory field (we don’t want pizzas that don’t fall into any of the categories), and that the field is searchable (so we can quickly view all pizzas from a certain category).
Let’s test what we have so far.
If you now edit a pizza in the pizza admin screen, you can see the dropdown-box with the categories. But you will notice immediately that the dropdown box only contains numbers! These are the id’s of the pizza_categories table. ATK can not guess which values it should use to display in the dropdown-box, so we have to tell it. We do this by adding a small function to the pizza_category class:
...
$this->setOrder("name");
}
function descriptor_def()
{
return "[name]";
}
}
The system automatically calls this function to determine what to display in the drop-down box when this node is used in a relation. What the function returns is actually a template. It’s a string that contains fieldnames between brackets. In this case, we use the ‘name’ field for the dropdown box. Had this been a ‘person’ node for example, we might have returned "[lastname], [firstname]". The fieldnames in brackets get replaced at runtime by fields from the table.
After adding this little function, re-open the pizza edit screen. Instead of a dropdown with numbers, you should now see a list of category names.
This completes the example of the many-to-one relation. We had to take some preparations at first, because we didn’t have any nodes to link to yet, but once we had created the node, we linked them using only very few lines of code.
One-to-many relation
Description
The 'opposite' of a many-to-one relation is the one-to-many relation. This relation is used when we speak of one pizza having a relation with many records from another node. (This type of relation is sometimes also called a 'master-detail' relationship.)
At first, I wanted to use 'ingredients' as a one-to-many example, but that isn’t a one-to-many,
but many-to-many relation (as you will see a few chapters from now).
We should take something simple as an example. So let’s add 'notes' to a pizza. A way for
the pizza bakers to add hints for their fellow bakers, like 'when you put tuna fish on this pizza,
leave out the spinach; it doesn’t combine well'. A pizza can have many notes, and each note
belongs to one specific pizza, so from the pizza point of view, there's a one-to-many relation
to notes.
Pizza 1 <-----------------> * Note
In the user-interface, a one-to-many relation is represented by a list of records in the edit screen of the master record, so in this case, a list of notes in the pizza edit screen.
Preparations
First, we create a table for storing the notes. For the purpose of this example, a note consists only of a text field (you might want to add an author, and a date later on). Of course we also need to add a primary key (id), and we need a field to store to which pizza the note belongs, which I would call pizza_id. (Note that in the previous chapter, I chose the name category over category_id. I did that because I wanted to name the field after the role it plays. In this case however, the field does not have any special meaning, it's just a way of linking notes to pizzas, and we will not even display the field.)
<sql> CREATE TABLE pizza_notes (
id int(10) NOT NULL, pizza_id int(10) NOT NULL, note text, PRIMARY KEY(id)
); </sql>
After executing this query on the database, we also need to create a node file for notes. There are a few things to notice about this node, which I will explain in a minute. Here's the implementation of the pizza_note node, which you should save as class.pizza_note.inc in the pizza module directory:
<?php
useattrib(“atktextattribute”);
userelation(“atkmanytoonerelation”);
class pizza_note extends atkNode
{
function pizza_note()
{
$this->atkNode("pizza_note");
$this->add(new atkAttribute("id", AF_AUTOKEY));
$this->add(new atkTextAttribute("note"));
$this->add(new atkManyToOneRelation("pizza_id", "pizzaman.pizza", AF_HIDE));
$this->setTable("pizza_notes");
$this->setSecurityAlias("pizzaman.pizza");
}
}
?>
The first thing to notice here is that the note contains a many-to-one relation. The current implementation of the one-to-many relation is such that it requires that there is a many-toone relation on the other side. So, if we want to create a one-to-many from a pizza to notes, there should be a many-to-one relation from notes to a pizza. We pass the AF_HIDE flag to this relation though, because we don’t want to display the pizza, as we will only see the notes in the context of the pizza to which they belong.
The second thing to notice is the line that follows the setTable statement. Remember that for every node we implemented until now, we added a menuitem() call and a registerNode() call to the module.inc file? Well, notes will not have a menu-item, since they will be edited from inside the pizza edit screen. The registerNode() call was used so we could grant rights to users to manage the node. You could add a registerNode() call for this node, so you could grant the right to edit notes to people. But in my opinion, if you are allowed to edit a pizza, you are also allowed to edit its notes. That’s where this last line, with the setSecurityAlias() function call, comes in. It makes the pizza_note node equal to the pizza node, from a security point of view. If someone wants to edit a note, the system will now check if this person has the right to edit a pizza. This helps to keep the number of checkboxes in the security profile screen of Achievo smaller.
Implementing the relation
As with the previous relation, the preparations are again more work than the actual implementation of the relation, but we must first create something to create a relation to.
We will implement a one-to-many relation in the pizza node, so open up the class.pizza.inc file.
Add the following line to the top of the file:
userelation("atkonetomanyrelation");
Right below the many-to-one relation 'category' that we added earler, we add the one-to-many-relation:
$this->add(new atkDateAttribute("entrydate"));
$this->add(new atkManyToOneRelation("category", "pizzaman.pizza_category",
AF_OBLIGATORY|AF_SEARCHABLE));
$this->add(new atkOneToManyRelation("notes", "pizzaman.pizza_note", "pizza_id",
AF_HIDE_LIST|AF_CASCADE_DELETE));
When comparing this line to the previous line, the many-to-one relation, you will notice that there's one extra parameter. The third parameter ("pizza_id" in our case), is the field in the target class that links back to this node. In other words, this is the field that the system uses to load all notes that belong to this pizza (pizza_id must be equal to the id of the current pizza). You might argue that the system should be able to determine this on its own, because of the many-to-one relation that we implemented in the pizza_note class. In a more complex system however, there may be more than one relation between two nodes (consider for example two many-to-one relations between projects and employees. One relation might indicate the technical coordinator of the project, whereas the other relation might indicate the account manager), so it's necessary to pass this third parameter, to link the correct field with this relation.
As fourth parameter to the one-to-many relation we pass two flags that you haven’t seen before. The AF_HIDE_LIST flag indicates that this field is not shown in the list of pizzas. In the pizza admin screen, we don’t want to see the notes from the pizzas (this would obfuscate the admin-screen), so we use this flag. The notes will become visible when we edit the pizza. The AF_CASCADE_DELETE flag makes sure that notes will be deleted from the database, if the pizza they belong to is deleted. This flag is often forgotten, leaving orphaned records in the database. Make sure you set the flag when needed.
Ok, that's it. You can now edit a pizza and add some notes to it, via the 'pizza note add' link in the edit screen (later on we will change the text to a more user-friendly string).
(todo, copy/pate from the pdf version, starting page 9)