Stand With Ukraine. Stop Putin. Stop War.

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.