Hello! Welcome to my humble web presence. I'm Mark Hamstra, 29 years young, wearer of many hats at modmore, resident of Leeuwarden, the Netherlands. Most of my time is spent building and maintaining awesome extras and tools at modmore, but I also love gaming, cooking, and my dogs. More about me.

This site is where I share thoughts, cool projects and other oddities related to MODX, xPDO and ExtJS. I also write about MODX regularly over at MODX.today. Sometimes I post three blogs in a week, sometimes there's nothing new here in a year. Read a random article.


You've probably heard of UUIDs as a way to have unique identifiers for objects. But have you heard of ULIDs?

I sure didn't until @PhilSturgeon tweeted about it, but since then I've taken a closer look and just finished implementing them into an xPDO project that was using UUIDs until now.

The basic premise is that, just like UUIDs, you can create the ULID without needing to know what the last one was (which is different from the standard primary auto-increment ID mostly used in xPDO projects). They're guaranteed to be unique, at least up to massive number of generations per millisecond. If the project needs to scale across multiple servers, or if you use it for request logging, that's a big plus. 

Comparing ULID vs UUID is more subjective, but I like that you get simpler and shorter identifiers. While a UUID would look something like 05c337c3-d2b3-4a50-a8b1-90e4fae23cfc, a ULID looks like 01edksqtx9cfzzt1y9sm57h3yq. It's still random gibberish, but it's cleaner and ready to be used in URLs.

If I understand it correctly, the first part of the ULID is based on the timestamp and will actually sort (roughly) by the time the ULID was generated, which is also a useful feat and may help with insert performance in databases.

To incorporate this into an xPDO project, you'll need to replace your xPDOSimpleObject usage with a custom base object and implement some logic related to the primary key.

For this example, I'm using robinvdvleuten/php-ulid. If you're following along, install that into your project with composer and make sure your autoloader has been loaded.

In your xPDO XML Schema, define a base object that all your other objects will extend from. Make note that this should extend xPDOObject (and not xPDOSimpleObject), and define the field to hold the primary key.

The examples in this article are based on xPDO 3, for use with xPDO 2 remove the namespaces (and perhaps add a custom prefix for your project to avoid conflicts) and it ought to work just the same.

<?xml version="1.0" encoding="UTF-8"?>
<model package="YourNamespace\Model\" baseClass="YourNamespace\Model\BaseObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
    <object class="YourNamespace\Model\BaseObject" extends="xPDO\Om\xPDOObject" inherit="single">
        <field key="ulid" dbtype="varchar" precision="52" phptype="string" null="false" />

        <index alias="ulid" name="ulid" primary="true" unique="true" type="BTREE">
            <column key="ulid" length="" collation="A" null="false" />
        </index>
    </object>
    
    
    <object class="SomeObject" table="some_object">
        ...
    </object>
</model>

As a ULID is encoded as a 26 character string, you can get away with lowering the precision to 26 but I like to have a little bit of padding in case things change down the road. 

Because I'm defining the baseClass in the model, I've skipped providing the extends attribute on the object. 

Also note that we're defining the index to be primary and unique.

Now build the model classes using a build script or for xPDO 3 the parse-schema command:

vendor/bin/xpdo parse-schema mysql path/to/your/project.mysql.schema.xml src/ -v --update=1 --psr4=YourNamespace

With the model files generated, edit your BaseObject class which should be in src/Model/BaseObject.php or model/yourproject/baseobject.class.php for xPDO 2.

What we're going to do is overriding the save() method to set the primary key for new objects with a freshly generated ULID.

(Again, note this is an xPDO 3 example. If using xPDO 2, remove namespaces and it should work the same.)

<?php
namespace YourNamespace\Model;

use Ulid\Ulid;
use xPDO\Om\xPDOObject;
use xPDO\xPDO;

class BaseObject extends xPDOObject
{
    public function save($cacheFlag = null)
    {
        if ($this->isNew()) {
            $this->set($this->getPK(), Ulid::generate(true));
        }
        return parent::save($cacheFlag);
    }
}

Pretty simple, huh? In this example I'm passing true to the generate method because I prefer the key to be lowercase. 

Now when you use the xPDO APIs to create new objects, it'll automatically add the ULID. And this will also work with your foreign keys and relations - just make sure to use a varchar field that can hold the ULID rather than an int like you may be used to with autoincrementing keys.

When retrieving objects note that you should use the array syntax on $xpdo->getObject to specify the key. xPDO might just be smart enough to handle it as we've defined the primary key, but for security reasons you should never pass arbitrary user data as a string into the second parameter of getObject

<?php

$ulid = (string)$_GET['ulid']; // or whereever you're getting this from, like a routing component
$object = $xpdo->getObject(\YourNamespace\Model\SomeObject::class, ['ulid' => $ulid]);
if ($object) {
    var_dump($object->toArray());
}
else {
    echo 'Doesn\'t exist';
}

Enjoy.

The Imagine PHP Library is a very useful tool for image manipulations. I'm using it on a variety of projects, including projects where users upload images directly from mobile devices, which are often rotated.

The actual rotation is stored in the EXIF data, but that's not automatically handled.

It turns out that implementing that with Imagine is really easy though, thanks to the Autorotate filter. 

Here's a functional example of taking an image, rotating it if needed, making sure it's at most 1920x1080px in size while keeping the aspect ratio, and finally stripping out the metadata.

<?php

// Create an Imagine instance - using Imagick if available otherwise falling down to GD
try {
    $imagine = new \Imagine\Imagick\Imagine();
} catch (\Imagine\Exception\RuntimeException $e) {
    $imagine = new \Imagine\Gd\Imagine();
}

// Use the Exif metadata reader to be able of accessing the orientation
$imagine->setMetadataReader(new \Imagine\Image\Metadata\ExifMetadataReader());

// Load the image into memory - this could also use $imagine->read or $imagine->open 
$img = $imagine->load('.. raw image data ..');

// Use the autorotate filter to rotate the image if needed
$filter = new \Imagine\Filter\Basic\Autorotate();
$filter->apply($img);

// Resize down to max 1920x1080px while keeping aspect ratio
$img = $img->thumbnail(
    new \Imagine\Image\Box(1920, 1920),
    \Imagine\Image\ManipulatorInterface::THUMBNAIL_INSET
);

// Strip off any metadata embedded in the image to save space and privacy
$img->strip();

// Render the binary data to store (or use $img->save() or $img->show())
$binary = $img->get('jpg');

When did you last check the size of your modx_session database table? Was it huge, like gigabytes huge? If so, you're not alone. 

To understand the problem, you need a little bit of background.

How do sessions work in PHP?

Typically, the standard PHP session handler will create a session when requested, and store it as a simple file somewhere on the system. The path it writes session to is configured with the session.save_path configuration in the php.ini, and can point anywhere. When that option is empty, it writes to the temp directory on the server. 

Creating and loading sessions is simple enough, but the next thing the session handler does is clean up sessions. This is called garbage control, or gc. This removes sessions beyond their expiration time, to make sure it doesn't keep growing indefinitely and takes up your vital disk space. 

Garbage control doesn't have to run on every request. If your session/cookie life time is configured to a week and you're not too picky about the exact timing they are removed, then sessions only really need to be checked once a day. Cleaning up sessions can take a little time and resources, so PHP is by default configured to only do that once every 100 requests.  

How do sessions work in MODX?

MODX registers a custom session handler that takes care of storing, retrieving, and cleaning up sessions. It writes this to one of its own database tables (modx_session), rather than files. This allows MODX a little more control over the flow of sessions. 

It is also possible to instruct MODX to use standard PHP sessions, and there's also an extra available to write sessions to Redis. But the vast majority of sites will simply be using the default, writing to the database.

So, why does MODX not clean up its session table?

MODX awaits the signal from PHP that it's time to clean up sessions. This relies on 2 configuration options in PHP:

  • session.gc_probability
  • session.gc_divisor

You can find the official documentation for those options here.

Usually the probability is set to 1, and divisor to a value like 100 or 1000. That means that in approximately 1/100 requests, the garbage control will run. 

When MODX does not seem to be cleaning up its table, it's usually because of an attempt to improve the performance of the garbage collection, by bypassing PHP and off-loading it to a cron job that runs outside of the request/response cycle. 

Those environments assume PHP writes its sessions to a standard location in the filesystem, and clean up that directory based on the timestamp on the file. The session.gc_probability option is then set to 0, to tell PHP to never run its own session garbage collection.

That works great - if your sessions are written to the standard location. Which MODX doesn't. 

How common is this?

Based on data from SiteDash, which checks the size and status of your session table automatically, it's pretty common indeed. Out of a sample of 1727 sites, 27% seem to be affected by this.

How can I fix this?

Re-enable session.gc_probability. Set it to 1, and make sure session.gc_divisor is also set properly for your traffic.

Depending on your host and if you have access to a server control panel, you may be able of changing it yourself. In other cases, contact your host and ask them how it should be changed. 

For a recent extra, I needed to get some arbitrary data into a package, in such a way that it's available for both the setup.options.php and the resolver - without duplicating that data. Specifically, it's a big array containing definitions for a theme with assets and elements that needed to be manually created/updated only if the user choose to do so.

After some time, I found a way to do that using the package attributes. And in this article I'll show you how to add that to a standard build.

Define the data

First, define the data. I created the file _build/data/theme.inc.php to return an array, but you can place it where it makes most sense. The file will only be accessed when building the package, so does not have to be in the core or assets folder (although it could be, if that makes sense for your use case).

<?php

$def = [
    // a whole bunch of data and elements
];

return $def;

Add the data to the package attributes

The package attributes is a special part of the package, which gets stored in the package manifest rather than a vehicle. It's used to hold some standard information: package name, changelog, readme, and license, among others.

In a standard build script the code to set the package attributes looks something like this:

<?php
// ... lots of other code ...
$builder->setPackageAttributes([
    'license' => file_get_contents($sources['docs'] . 'license.txt'),
    'readme' => file_get_contents($sources['docs'] . 'readme.txt'),
    'changelog' => file_get_contents($sources['docs'] . 'changelog.txt'),
    'setup-options' => [
        'source' => $sources['build'] . 'setup.options.php',
    ],
]);

It turns out though - package attributes are not limited to those standard items. Any attribute will be stored into the package manifest.

Let's take advantage of that by adding our own attribute containing (in this case) a theme-definition from our file:

<?php
$builder->setPackageAttributes([
    'license' => file_get_contents($sources['docs'] . 'license.txt'),
    'readme' => file_get_contents($sources['docs'] . 'readme.txt'),
    'changelog' => file_get_contents($sources['docs'] . 'changelog.txt'),
    'setup-options' => [
        'source' => $sources['build'] . 'setup.options.php',
    ],
    'theme-definition' => json_encode(include __DIR__ . '/data/theme.inc.php'),
]);

As the theme definition returns an array, we're simply include-ing it. I decided to encode it as JSON, but I don't think you have to do that - the package manifest is serialised so should also support arbitrary arrays.

If you were to build a package at this point, that would include the theme-definition, but it's not being used yet.

Accessing package attributes in setup.options.php

In the _build/setup.options.php file, which is used to build the interface for the setup options shown when installing a package, the package attributes are available in $options['attributes'].

For example, to retrieve the theme-definition, the code would look like this:

<?php
$def = array_key_exists('theme-definition', $options['attributes']) 
    ? json_decode($options['attributes']['theme-definition'], true)
    : [];

if (empty($def) || !is_array($def)) {
    return 'Failed to load theme definition: ' . json_encode($options, JSON_PRETTY_PRINT);
}

foreach ($def as $definition) {
    // ... render the option ...
}

Now you can build a dynamic interface based on your data definition. We return an error to the setup options panel if we can't find the attribute.

Access data in resolvers

Building the interface is step one - accessing the same information in a resolver is step two.

In resolvers, the package attributes are in $options.

<?php

$def = array_key_exists('theme-definition', $options) 
    ? json_decode($options['theme-definition'], true) 
    : [];

if (empty($def) || !is_array($def)) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'Failed to load theme definition');
    return false;
}

The selected values in the setup options window are also available in $options. So if you created a setup option named "create_template", you can check that like so:

<?php

$def = array_key_exists('theme-definition', $options) 
    ? json_decode($options['theme-definition'], true) 
    : [];

if (empty($def) || !is_array($def)) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'Failed to load theme definition');
    return false;
}

if (array_key_exists('create_template', $options) && $options['create_template']) {
    foreach ($def['templates'] as $template) {
        // ... create the template or something ...
    }
}

Especially for use cases like themes, or where you have some dynamic data you want to manually create/update in a resolver instead of as a vehicle, this can be a useful technique to have under your belt.

Patreon is a community membership service that lets you pledge monthly donations, at a price you set yourself, to creators.

Back in December 2017 I first created a Patreon account to support Vasily "bezumkin" Naumkin with his work on the MODX core. Shortly after that I added pledges to the creators of PHPunit, FlySystem, and Wait But Why (which has nothing to do with programming, but is just one of my favourite blogs on the internet). At a later point in 2018 I also pledged to Homebrew.

They're all small amounts, more a token of my support and gratitude than anything else. The largest part was for Vasily ($25, until July), and the others were $3-5 each for a total of $38.

There are no tangible benefits, other than making sure that the software doesn't just go away by rewarding the creator. I routinely spend more money on stupid things I don't really need (like a huge foam enter button), while what these creators share with the world is much more valuable, so that's a really good deal.

They are also recurring, automatically charged monthly, which starts to add up over time.

In 2018 my personal Patreon donations totalled at $348. Still not enough to pay anyone's bills or full time employment, but probably more than I would've donated to these projects if they only accepted one-off donations, and the numbers do get more meaningful for the creator when you consider the effect of more people contributing. Previously these creators would fund their work through client work, sacrifice free time after a day job, rely on one-time donations, or juggle things in another way, while with Patreon they're offered a predictable (extra) income directly attributable to the work they share.


After a chat about the work that goes into my open source projects and in particular the work that goes into the MODX core, I decided to set up my own Patreon yesterday.

I am fortunate enough that I have a business that's running well and pays the bills, but I still constantly have to prioritise my time and energy. When the to do list explodes, or energy gets low (yay, burnout), I have to choose what things to work on. The reality of running a business is that things that make money are more important than things that don't, and as a result it's usually the open source work that gets snoozed first even though I strongly believe that the work I do on the MODX project should be an important part of my day-to-day work.

So that's where Patreon comes in.

By supporting me on Patreon, you're supporting my work for the MODX core and open source extras. Every contribution shows that these hours upon hours of work are worth something to you, and that will motivate me to keep up and make more time available to keep making the CMS you use better, one PR at a time.


If you're interested in supporting others in the MODX community, check out Joshua L├╝ckers' Patreon, who is the most recent person to become a MODX integrator and has been putting in lots of hours as well.

I'm not aware of any other MODXers with a Patreon page at the moment (Vasily shut his down in July), but if you find any, or have any other Patreons you support, leave a comment below :)

For SiteDash I built a worker queue, based on MySQL, to handle processing tasks asynchronously. There's a central database and separate worker servers inside the same private network that poll for new tasks to execute. These worker servers run PHP, using xPDO 3, to perform the tasks the application server has scheduled.

One problem that would occasionally pop up is that the worker servers would lose connection with the database. The database is on a different server in the network, so that could come from rebooting the database server, a deploy or backup causing high load, network glitch, or just.. gremlins.

Obviously, the worker servers need to talk to the database to be useful, so I started looking at a way to 1) detect the connection was dropped and 2) automatically reconnect if that happens. It turns out to be fairly straightforward (once you know how!).

First, I implemented a check to see if the connection is alive. It does that by checking if a prepared statement (query) could be prepared.

<?php
while (true) {
    $q = 'query that is irrelevant here';
    $stmt = $xpdo->query($q);
    if ($stmt) {
        $stmt->execute();
    }
    else {
        reconnect();
    }
    // Execute task, if any
    sleep(1);
}

function reconnect() {
    global $xpdo;
    $xpdo->connection->pdo = null;
    return $xpdo->connect(null, array(xPDO::OPT_CONN_MUTABLE => true));
}

The workers run in an infinite loop, one loop per second, so this check happens every second. When the statement can't be prepared it's treated as a dropped connection, and we call the reconnect method to restore the connection.

The reconnect happens by unsetting the PDO instance on the xPDOConnection instance. Without that, xPDO thinks it still has a connection, and will continue to fail. Because we don't unset the xPDOConnection instance, we can just call $xpdo->connect() without providing the database connection details again.

With this check in place, the loop can still get stuck in a useless state if there's a reason it can't reconnect. That can have some unintended side effects and makes it harder to detect a problem that needs manual interference, so I also implemented another check.

Every 10 loops, another query is sent to the database with a specific expected response; a simple SELECT <string>. The idea is the same as the check above, see if the statement can't be prepared or doesn't return the expected result, and if so, do something.

Here's what that roughly looks like:

<?php
$wid = 'Worker1';
$loops = 0;
while (true) {
    $loops++;
    
    $q = 'query that is irrelevant here';
    $stmt = $xpdo->query($q);
    if ($stmt) {
        $stmt->execute();
    }
    else {
        reconnect();
    }
    // Execute task, if any
    sleep(1);
    
    // Every 10 loops, check if the connection is alive
    if (($loops % 10) === 0) {
        $alive = $xpdo->query('SELECT ' . $xpdo->quote($wid));
        if (!$alive || $wid !== $alive->fetchColumn()) {
            break;
        }
    }
}

function reconnect() {
    global $xpdo;
    $xpdo->connection->pdo = null;
    return $xpdo->connect(null, array(xPDO::OPT_CONN_MUTABLE => true));
}

In this case, we're not calling the reconnect() method. Instead, we're breaking out of the loop. This way the PHP process can end gracefully, instead of pretending to be churning along properly. When the process ends, supervisord is used to automatically restart it. When a new process is unable of connecting, the logs and monitoring get a lot louder than when a worker silently keeps running, so this system is working nicely.

Now, this obviously isn't the entire worker code for SiteDash. Over time it has grown into 300 lines (not counting the tasks themselves) of worker logging, automatic restarting when a deployment happened, analytics, ability to gracefully kill a process, and dealing with unexpected scenarios like a database connection getting dropped.

Overall this system has managed to keep the processes running quite nicely. There were some issues where certain tasks would cause a worker to get stuck, which have now been resolved, and currently the biggest limiting factor for the worker uptime is deployments. The workers need to restart after a deployment to make sure there is no old code in memory, and I have been fairly busy with adding features to SiteDash (like remote MODX upgrades last week!).

It's also been fun and interesting to try to get a good insight into these background processes and tweaking the monitoring to notify about unexpected events, without triggering too many false negatives. A challenge I'd like to work on in the future is automatically scaling the number of workers if the queue goes over a certain threshold, but for now I can manually launch a couple of extra processes quite quickly if things take too long.

Some fun numbers:

  • Overall, since launching workers as indefinitely running processes, the average worker process was alive for 12,5 hours
  • Since fixing the last known glitch where a process could get stuck executing certain tasks, on October 29th, the average worker stayed online for 2 days, 12 hours and 52 minutes.
  • The longest running workers started on November 1st and stayed up for 12 days, 5 hours and 40 minutes before being restarted due to a deployment.