Creating simple extras for easy snippet management

I have a couple of snippets that I use on multiple sites, and have been copy and pasting them from site to site. It gets frustrating as I make updates to one, it isn’t reflected in the other sites.

Is there an easy way to package up a group of snippets into an extra to make it easier to update through out these multiple projects?

edit: They don’t need any sort of DB or schema, they only use the $modx object a tiny amount, primarily to add/get stuff from the cacheManager

edit 2: I know there’s a couple like MyComponent, ExtraBuilder, and PackMan, but they all seem too involved given the snippet:

Said snippet
 * Generate the sizes and srcset attributes automatically
 * based on passed options similar to the pthumb &options field
if (empty($input)) {

$use_cache = $modx->getOption('useCache', $scriptProperties, 1);
$cache_key = json_encode($scriptProperties).'-imagesizes';

if (!empty($use_cache)) {
    $cached_result = $modx->cacheManager->get($cache_key);
    if (!empty($cached_result)) return $cached_result;

$pt_settings = [];

if (empty($pt_settings)) {
    if (!$modx->loadClass('phpthumbof', MODX_CORE_PATH . 'components/phpthumbof/model/', true, true)) {
        $modx->log(modX::LOG_LEVEL_ERROR, '[imageSizes] Could not load phpThumbOf class.');
        return 'Could not load phpthumbof for '.$input;

$pThumb = new phpThumbOf($modx, $pt_settings, $scriptProperties);

$sizes_options = $modx->getOption('sizes', $scriptProperties, 1);

$bp_options = $modx->getOption('bp', $scriptProperties);
$img_options = $modx->getOption('options', $scriptProperties);
$default_options = $modx->getOption('default', $scriptProperties, null);
$dims_option = $modx->getOption('dims', $scriptProperties, 0);

$lazy_option = $modx->getOption('lazy', $scriptProperties, 0);
$async_option = $modx->getOption('async', $scriptProperties, 0);

if (!$async_option) {
    /** Originally the async option was set using "decoding" rather than "async" */
    $modx->log(MODX::LOG_LEVEL_ERROR, '[imageSizes] The option "decoding" is deprecated, use "async" instead.');
    $async_option = $modx->getOption('decoding', $scriptProperties, 0);
$use_sizes = $sizes_options && !str_contains($bp_options, 'x');
$use_webp = $modx->getOption('useWebp', $scriptProperties, 0);
$img_options = trim($img_options);

if (!empty($img_options)) {
    $img_options = explode(',', $img_options);
} else {
    $modx->log(MODX::LOG_LEVEL_ERROR, 'Image Options is empty, make sure there isn\'t an issue with passing in the &options parameter');
    return 'src="'.$input.'" '.'onload="console.error(`[imageSizes] &options is empty, using $input: '.$input.' as src instead.`)"';
$srcAttr = ' src="';
$sizesAttr = ' sizes="';
$srcsetAttr = ' srcset="';

if (!empty($bp_options)) {
    $i = 0;
    foreach(explode(',', $bp_options) as $breakpoint_option) {
        $img_option = $img_options[$i].'&dims=1';
        if (!empty($use_webp)) $img_option .= "&f=webp";
        $src = $pThumb->createThumbnail($input, $img_option);
        $dims = getimagesize($src['file']);
        $src = $src['src'];
        $w = $dims[0];
        $h = $dims[1];
        if ($use_sizes) {
            $sizesAttr .= '(min-width: '.$breakpoint_option.'px) '.$w.'px';
        if (!strpos($breakpoint_option, 'x')) {
            $srcsetAttr .= $src.' '.$w.'w';
        } else {
            $srcsetAttr .= $src.' '.$breakpoint_option;
        $i += 1;
        if ($i != sizeof($img_options)) {
            if ($sizes_options) {
                $sizesAttr .= ', ';
            $srcsetAttr = $srcsetAttr.', ';
        } else {
            if ($sizes_options) {
                $sizesAttr .= '" ';
            $srcsetAttr .= '" ';
            if ($default_options) {
                $img_option = $default_options.'&dims=1';
                if ($use_webp) $img_option .= '&f=webp';

                $src = $pThumb->createThumbnail($input, $img_option);
                $dims = getimagesize($src['file']);
                $src = $src['src'];
                $w = $dims[0];
                $h = $dims[1];
            $srcAttr .= $src.'" ';
            if ($dims_option) {
                $srcAttr .=  'width="'.$w.'" height="'.$h.'" ';

if ($use_sizes) {
    $output = $sizesAttr . $srcsetAttr . $srcAttr;
} else {
    $output = $srcsetAttr . $srcAttr;

if ($lazy_option) {
    $output .= ' loading="lazy"';
if ($async_option) {
    $output .= ' decoding="async"';

$cache_time = 2592000; // 30 days
$modx->cacheManager->set($cache_key, $output, $cache_time);

return $output;

If you want to create a transport package from the manager, then there is no easier way than PackMan.

If you want to create a ‘proper’ extra (with files in a folder you can manage with Git), then take a look at Git Package Management. (It looks a bit intimidating but isn’t that hard to use, once you managed to install it.)

Okay, so it sounds like PackMan is the solution, but is it safe to use in MODX3? I’m hesitant to use extras that don’t explicitly say “works in v3” somewhere.

PS: Is there an ELI5 for what a transport package is? Is it just a fancy .zip?

I don’t think PackMan works in MODX 3.
(You could create the package on a local MODX 2.x installation though, and use that package in MODX 3 as well.)

A snippet in MODX is more than just the code. There is also some meta data like name, description or what category it belongs to. All that information is then written to different files in a format that MODX understands together with information about the package and zipped.

Ahhhh, that makes sense. I will go ahead and try out PackMan like you said. It does seem to be the right solution. Thanks!

This topic was automatically closed 2 days after discussion ended and a solution was marked. New replies are no longer allowed. You can open a new topic by clicking the link icon below the original post or solution and selecting “+ New Topic”.