We think over the logic of work, determine the schema and model of the database
Last updated Oct 11th, 2026 | Page history | Improve this page | Report an issue
Support the team building MODX with a monthly donation.
The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.
Backers
Budget
$194 per month—let's make that $500!
Learn morePreparation is over — we start building the component: a mailing-list extra. The goal of the course is learning to write components for MODX, not building the best newsletter in the world; keep the scope small.
The design:
- Newsletter — everything that forms a letter: subject, template, sender.
- Subscriber — a subscription to a newsletter. For now the subscriber must be an authorized user; guests can be added later.
- On certain events a snippet generates the letters and stores them as Queue objects.
- A scheduled script (every 5 minutes at least) sends emails from the queue. An empty queue means everything is out.
The manager gets pages for managing subscriptions, adding subscribers and viewing the message queue — with delete and send-now actions. A page for composing and test-sending letters may come later.
With the design fixed, write the database schema.
DB table schema¶
The schema is an XML file that describes objects and their relations. It is not used at runtime — it only generates the model.
The schema will change as the component grows: objects, indexes and relations get added and removed. Do not try to design every column upfront; columns can be added at any time.
The basics of the xPDO schema are in the official documentation. Below is the finished file from the course repository.
Each object is an object tag. Its attributes carry the class name and the database table. The table is specified without the site prefix — MODX adds it when needed:
<object class="sxNewsletter" table="sendex_newsletters" extends="xPDOSimpleObject">
The object extends xPDOSimpleObject, which supplies the common CRUD surface — but in MODX 3 the id column is not inherited automatically: xPDOSimpleObject is an empty class and the generator does not inject an id field. Declare the primary key explicitly in the schema, the same way core does:
<field key="id" dbtype="int" precision="10" attributes="unsigned,auto_increment" phptype="integer" null="false" index="pk" generated="native" />
Without this field the generated table is created without id and without a primary key. If you extend plain xPDOObject instead, you also have to declare every other shared column yourself — for most components xPDOSimpleObject plus an explicit id is the right choice.
Fields follow:
<field key="name" dbtype="varchar" precision="100" phptype="string" null="false" default="" />
<field key="description" dbtype="text" phptype="text" null="true" default="" />
<field key="active" dbtype="tinyint" precision="1" phptype="boolean" attributes="unsigned" null="true" default="1" />
| Attribute | Meaning |
|---|---|
key |
field name |
dbtype |
database type: int, varchar, text, … |
precision |
field size; required for fixed-length types (int, varchar), omitted for text |
phptype |
PHP type the value is cast to: integer, string, float, json, array |
null |
whether the field may be empty; with false, a missing value on save writes an error to the log |
default |
value used when the field can be null and nothing was passed on save |
attributes |
extra column properties passed through to MySQL |
json and array are MODX-specific: array serializes data with type preservation via serialize() / unserialize(), json uses json_encode() / json_decode(). Arrays store in the database and come back as arrays.
These are the basic attributes only — MODX supports more; look at core schemas and copy what you need.
Indexes come next, so the table works fast. Usually it is enough to index the fields that get selected:
<index alias="name" name="name" primary="false" unique="false" type="BTREE">
<column key="name" length="" collation="A" null="false" />
</index>
<index alias="active" name="active" primary="false" unique="false" type="BTREE">
<column key="active" length="" collation="A" null="false" />
</index>
| Attribute | Meaning |
|---|---|
primary |
primary index? Usually not — the primary index is on the id field from xPDOSimpleObject |
unique |
unique index? If true, no two rows share a value; id is unique again |
Finally, object relations:
<composite alias="Subscribers" class="sxSubscriber" local="id" foreign="newsletter_id" cardinality="many" owner="local" />
<aggregate alias="Template" class="modTemplate" local="template" foreign="id" cardinality="one" owner="foreign" />
<aggregate alias="Snippet" class="modSnippet" local="snippet" foreign="id" cardinality="one" owner="foreign" />
- composite — this object is the parent; deleting it deletes all child objects linked here.
- aggregate — this object points to another object; when that object is deleted, the link breaks.
-
alias — the relation name used in
$object->getMany('Subscribers')or$object->addOne('Template'). - class — the real class name of the related object.
- local / foreign — the fields the relation joins on: on this object and on the related one.
-
cardinality — one-to-one or one-to-many. Aggregates are usually
one, composites usuallymany— a parent has many children, a child has one parent — but there are exceptions. FormanyuseaddMany()/getMany(), foroneuseaddOne()/getOne().
For a visual view of the schema, use the service from Jeroen Kenters:

The schema will change several times during development — it gets clearer as we go.
Model generation¶
The schema alone does nothing; the working part is the model. The database model in MODX is a set of PHP files: the base object classes plus database-specific extensions.
Generate the model:
- Copy the finished schema into the project and save; synchronize with the server.
- Delete the old modExtra model files:

-
Run
_build/build.phpon the server (for examplephp ~/www/Extras/Sendex/_build/build.php— model generation is the privatebuildModel()method inside it, called fromprocess()). The first run prints only done; later runs warn that existing objects will not be overwritten. -
Synchronize the model directory back (the two green arrows at the top):


- The files arrive in the project brown — not yet in Git. Add them via the context menu; they turn green:


- Register the new objects for install in
/_build/resolvers/resolve.tables.php:

The white bar left of the line numbers marks changed lines; the file turns blue until the changes are committed.
- Commit:

The diff shows the old and the new version of each file — review it before sending.
Here is today's commit with all the work, and the full commit list to track progress.
The generated model, on the example of sxNewsletter:
-
/model/sendex/metadata.mysql.php— the list of objects in the component. -
/model/sendex/sxnewsletter.class.php— thesxNewsletterobject with its main methods. -
/model/sendex/mysql/sxnewsletter.class.php— the MySQL platform class: database-specific methods and the object map. Since MODX 3 the map is no longer a separate*.map.inc.phpfile — the generator writes it into the class aspublic static $metaMap = [...](fields, indices and relations from the XML schema).
A second schema, say for MsSQL, would keep metadata.mysql.php unchanged and add a /model/sendex/mssql/ directory with its own sxnewsletter.class.php containing the MsSQL map. This is how xPDO supports any database: one common object, extended per database.
The files under /model/sendex/mysql/ are regenerated from the schema on every build — do not edit them. Custom methods go into /model/sendex/sxnewsletter.class.php and are called like this:
if ($newsletter = $modx->getObject('sxNewsletter', 1)) {
echo $newsletter->nameMethod ('options');
}
Look at modUser: isAuthenticated() and joinGroup() are exactly such methods — this is how MODX works. You now know how to inspect any engine or extra object.
sxSubscriber and sxQueue work the same way.
Conclusion¶
Today: the component design and the first version of the xPDO model for MySQL.
Next lesson: assemble the transport package, install it on the site and set up convenient development. Then custom manager pages and the ExtJS interface.
Support the team building MODX with a monthly donation.
The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.
Backers
Budget
$194 per month—let's make that $500!
Learn more










