This is Part 3 of my series on Writing Components For CiviCRM. Part 1
discussed what a component is, and what it does. Part 2
explained how to use CiviCRM's XML-based data definition language to define your tables. But while your tables may now be in place, CiviCRM doesn't know what to do with your data. We'll need to do a bit of editing on CiviCRM Core in order to hook up your data in CiviCRM, and I'll briefly describe some of the places we will need to "hack" core so that your data will appear in the UI.
So what do I mean by "hooking up data"? CiviCRM needs to know when to load your code so that users of your components can do things like:
- View your data via the contact view page.
- Edit your data in contact edit pages.
- Search for contacts using your data as keys.
- Access your data via Drupal modules or similar external code.
- Import and export data that will ultimately be stored in your tables.
- Implement special menu handlers if we display special pages, so CiviCRM will Invoke the code to render our pages.
- Be able to create and delete data objects you create, and have your objects get created/deleted as needed when a contact gets created or deleted.
- Set any administrative settings your component needs.
Truth in advertising here: I've only done some of the above, and not everything I've tried works in Voter. In particular, Import has proven more difficult that I expected, and Search needs work as well (If you're interested in helping push this work forward for Voter and can help pay for the work, I'd love to have you as a sponsor, BTW), and Edit is pretty rough. But I'll use my work on CiviVoter as a case study to demostrate some of the things you need to do.
First Steps Hooking Up
I've already alluded to the first part of the hook up: you need to write BAO (business logic) classes to handle the "higher level" parts of what it takes to manipulate your objects in the database. In Voter's case, we need make sure that contact-related data gets loaded whenever its corresponding contact is loaded (CiviCRM does not do this for us automatically
in the 1.x versions), and we need to add code to the Canvass class to manage assigning voters to a canvass (so we can contact them), assigning volunteers to blocks of voters within a canvass (so voters are contacted only once), and to allow our code to get a link to a voter so a volunteer can make a call, or so we can put the voter's information on a "walk list" before we send the volunteer door-to-door to talk to voters. All of this core logic goes into one or the other BAO class, except for a few places where we need to "patch" core CiviCRM to get our code to execute.
To do these things, almost all BAO classes will need to define the following functions. In CiviCRM 1.x, most of these are "static", or "class" methods of your BAO class; I'll point out the exceptions when we get to these.
The core methods are:
- static &create(&$params, &$ids)
- Creation and update code for an object that has "sub-objects". The $ids field gives us back the CiviCRM IDs of these sub-objects.
- static retrieve( &$params, &$defaults )
- This is the easiest interface to use in order to retrieve a single record from the database as a BAO object.
- static exportableFields( )
- This is part (although, unfortunately, not all of what you need to implement in order to let the standard exporter export your data).
My sense is that there are probably more of these, but since this is currently undocumented in 1.x, we'll likely need to wait for 2.0 for clearer instructions here. But we can think of these core functions as a "pseudo-interface" for the BAO class. This means that the code that calls the BAO class expects these functions to be there, and you will likely get run-time errors if they are not implemented or behave differently than CiviCRM expects.
For those of you familiar with Java or PHP 5, you'll notice that I said "pseudo interface" and not "interface". This is because CiviCRM supports PHP 4, which does not have an interface
keyword. In PHP 5, we can guarantee that a class really implements an interface, and the interface
statement documents clearly what that interface is, although you still have to make sure that your functions behave as the calling class expects. In PHP4, you just have to "know" what you need to implement for things to work right. In PHP 5, it's a lot more clear.
CiviCRM uses this particular "design pattern" throughout its code base, and implementing interfaces is most of what you need to do as a component writer.
In addition, CiviVoter also needs data objects to have certain relationships to each, including:
- Voter objects need to create a VoterInfo object to store Campaign-specific information for that voter.
- Canvass objects need to be able to retrieve the Voter objects assigned to them. I have a couple of methods for this important operation, including an "iterator" method that makes it easier to retrieve sequences of voters.
- Canvass objects need to be able to retrieve the various voter contact records for that canvass.
- Canvass object need to "lock" Voter objects so they do not get assigned to multiple volunteers.
All of these methods are implemented within my BAO classes, which is typical for other components as well.
It would be nice if this were all you had to do, and it's possible that in 2.0 that this will be the case. But you also need to make a couple of modifications to Core as well:
- In create() needs to handle the 'Voter' contact_subtype when we create Voter records
- deleteContact() and importableFields() need special handling for Voters as well.
- setDefaultValues(), buildQuickForm() and postProcess() all require modification before we can edit Voter objects in the UI.
We'll get to more of these when we talk about search. In addition, I suspect that both Export and Import may require modifications I have not figured out yet.
That's a long post. We'll talk about UI in the next entry.