Issue retrieving data from a generated custom package in modx 3

I recently upgraded an older site from 2.8 to 3.1.2, as well as to use PHP 8.1 - that all went well, I found all the issues and fixed them.

The next thing I wanted to do was upgrade my custom package to use the modx 3 methods for generating, updating and adding packages.

So I decided to start fresh and created a new file structure that looks like the one below:

  core/
    components/
      studioapp/
        src/
          Model/
        schema/
        bootstrap.php

I copied my old XML file into the new schema directory and updated it to look like the new format:

<?xml version="1.0" encoding="UTF-8"?>
<model package="studioapp\Model" baseClass="xPDO\Om\xPDOObject" platform="mysql" defaultEngine="InnoDB" version="3.0"> 
	<object class="Activity" table="ods_activity" extends="xPDO\Om\xPDOSimpleObject">
		...
	</object>
</model>
</xml>

I created a new namespace that looks like this:

name: studioapp
core path: {core_path}components/studioapp/
assets path: {assets_path}components/studioapp/

I created a build script in my _build directory that looked like this:

<?PHP

// Set API Mode to true
if (!defined('MODX_API_MODE')) {
    define('MODX_API_MODE', true);
}

// Include the main index.php file to load MODX in API Mode
@include(dirname(__FILE__, 3) . '/index.php');

/**
 * @var \MODX\Revolution\modX $modx
 *
 */

$config_core_path = dirname(__FILE__) . '/config.core.php';
if (!file_exists($config_core_path)) {
    $config_core_path = dirname(dirname(__FILE__)) . '/config.core.php'; // Try one level up if script is in a subdir
    if (!file_exists($config_core_path)) {
         echo "FATAL ERROR: config.core.php not found. Tried:\n";
         echo "- " . dirname(__FILE__) . "/config.core.php\n";
         echo "- " . dirname(dirname(__FILE__)) . "/config.core.php\n";
         echo "Please adjust path if necessary.\n";
         exit(1);
    }
}

echo "Attempting to load config from: " . $config_core_path . "\n";
require_once $config_core_path;
echo "SUCCESS: {$config_core_path} included.\n";

if (!defined('MODX_CORE_PATH')) {
    echo "ERROR: MODX_CORE_PATH was not defined by config.core.php!\n";
    exit(1);
}
echo "MODX_CORE_PATH defined as: " . MODX_CORE_PATH . "\n";

require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
echo "SUCCESS: modx.class.php included.\n";

$modx = new modX();
if (!$modx) {
    echo "FATAL ERROR: Failed to instantiate modX object.\n";
    exit(1);
}
echo "SUCCESS: modX object instantiated.\n";

$modx->initialize('mgr'); // Initialize 'mgr' or 'cli' context
echo "SUCCESS: MODX context 'mgr' initialized.\n";

echo "Attempting to connect to the database...\n";
if (!$modx->connect()) {
    echo "ERROR: Failed to connect to the database. Check core/config/config.inc.php credentials and DB server status.\n";
    exit(1);
}
echo "SUCCESS: Database connection confirmed.\n";


// --- Schema Parsing Logic ---
$base_path = MODX_CORE_PATH . 'components/studioapp/';
$schema_file = $base_path . 'schema/studioapp.mysql.schema.xml';

// Package name must match the schema exactly
$package_name = 'studioapp\Model';

// Point to the src directory
$model_output_path = $base_path . 'src/';

// Add namespace registration
$namespacePrefixForPackage = 'studioapp\\';
$modx->addPackage($package_name, $model_output_path, $tablePrefix, $namespacePrefixForPackage);

$modx->setLogLevel(modX::LOG_LEVEL_INFO);
$modx->setLogTarget('ECHO');

//$modx->log(modX::LOG_LEVEL_INFO, "[ModelGen] Starting model generation for package: {$package_name}");

if (!is_file($schema_file)) {
    $modx->log(modX::LOG_LEVEL_ERROR, "[ModelGen] Schema file not found at: {$schema_file}");
    exit("Script aborted: Schema file not found.\n");
}

if (!is_writable($model_output_path)) {
    $modx->log(modX::LOG_LEVEL_ERROR, "[ModelGen] model directory is not writable: {$model_output_path}");
    exit("Script aborted: Directory not writable.\n");
}

$modx->log(modX::LOG_LEVEL_INFO, "[ModelGen] Using schema: {$schema_file}");
$modx->log(modX::LOG_LEVEL_INFO, "[ModelGen] Output path: {$model_output_path}");


if (!is_writable($model_output_path)) {
    $modx->log(modX::LOG_LEVEL_ERROR, "[ModelGen] model directory is not writable: {$model_output_path}");
    exit("Script aborted: Directory not writable.\n");
}

$modx->log(modX::LOG_LEVEL_INFO, "[ModelGen] Using schema: {$schema_file}");
$modx->log(modX::LOG_LEVEL_INFO, "[ModelGen] Output path: {$model_output_path}");

/** @var xPDOManager $manager */
$manager = $modx->getManager();
/** @var \xPDO\Om\xPDOGenerator $generator */
$generator = $manager->getGenerator();


echo("Parsing schema: $schema_file".PHP_EOL);
// Parse the schema to generate the class files
$generator->parseSchema(
    $schema_file,
    $base_path . 'src/',
    [
        "compile" => 0,
        "update" => 0,
        "regenerate" => 1,
        "namespacePrefix" => "studioapp\\"
    ]
);

Which worked and generated my class files in the Model directory.

This is my metadata.mysql.php file that was generated

<?php
$xpdo_meta_map = array (
    'version' => '3.0',
    'namespace' => 'studioapp\\Model',
    'namespacePrefix' => 'studioapp',
    'class_map' => 
    array (
        'xPDO\\Om\\xPDOSimpleObject' => 
        array (
            0 => 'studioapp\\Model\\Activity',
            ...
        ),
    ),
);

The activity class file:

<?php
namespace studioapp\Model;

use xPDO\xPDO;

/**
 * Class Activity
 *
 * @property integer $user_id
 * @property integer $page_id
 * @property integer $resource_id
 * @property string $resource_type
 * @property string $activity
 * @property string $activity_type
 * @property string $activity_time
 * @property string $activity_description
 *
 * @package studioapp\Model
 */
class Activity extends \xPDO\Om\xPDOSimpleObject
{
}

And it’s MySQL file:

<?php
namespace studioapp\Model\mysql;

use xPDO\xPDO;

class Activity extends \studioapp\Model\Activity
{

    public static $metaMap = array (
        'package' => 'studioapp\\Model',
        'version' => '3.0',
        'table' => 'ods_activity',
        'extends' => 'xPDO\\Om\\xPDOSimpleObject',
        'tableMeta' => 
        array (
            'engine' => 'InnoDB',
        ),
        'fields' => 
        array (
            'user_id' => NULL,
            'page_id' => NULL,
            'resource_id' => NULL,
            'resource_type' => '',
            'activity' => NULL,
            'activity_type' => '',
            'activity_time' => 'CURRENT_TIMESTAMP',
            'activity_description' => '',
        ),
        'fieldMeta' => 
        array (
            'user_id' => 
            array (
                'dbtype' => 'int',
                'precision' => '11',
                'attributes' => 'unsigned',
                'phptype' => 'integer',
                'null' => false,
            ),
            'page_id' => 
            array (
                'dbtype' => 'int',
                'precision' => '11',
                'attributes' => 'unsigned',
                'phptype' => 'integer',
                'null' => false,
            ),
            'resource_id' => 
            array (
                'dbtype' => 'int',
                'precision' => '11',
                'attributes' => 'unsigned',
                'phptype' => 'integer',
                'null' => false,
            ),
            'resource_type' => 
            array (
                'dbtype' => 'varchar',
                'precision' => '25',
                'phptype' => 'string',
                'null' => false,
                'default' => '',
                'comment' => 'DANCER,CLASS,PARENT,SEASON,TEACHER,INVOICE,ETC',
            ),
            'activity' => 
            array (
                'dbtype' => 'varchar',
                'precision' => '25',
                'phptype' => 'string',
                'null' => false,
            ),
            'activity_type' => 
            array (
                'dbtype' => 'varchar',
                'precision' => '25',
                'phptype' => 'string',
                'null' => false,
                'default' => '',
                'comment' => 'CREATE,UPDATE,DELETE',
            ),
            'activity_time' => 
            array (
                'dbtype' => 'timestamp',
                'phptype' => 'timestamp',
                'null' => false,
                'default' => 'CURRENT_TIMESTAMP',
            ),
            'activity_description' => 
            array (
                'dbtype' => 'varchar',
                'precision' => '255',
                'phptype' => 'string',
                'null' => false,
                'default' => '',
                'comment' => 'Resource X was Updated from Value 1 to Value 2',
            ),
        ),
        'indexes' => 
        array (
            'resource_id' => 
            array (
                'alias' => 'resource_id',
                'primary' => false,
                'unique' => false,
                'type' => 'BTREE',
                'columns' => 
                array (
                    'resource_id' => 
                    array (
                        'length' => '',
                        'collation' => 'A',
                        'null' => false,
                    ),
                ),
            ),
            'user_id' => 
            array (
                'alias' => 'user_id',
                'primary' => false,
                'unique' => false,
                'type' => 'BTREE',
                'columns' => 
                array (
                    'user_id' => 
                    array (
                        'length' => '',
                        'collation' => 'A',
                        'null' => false,
                    ),
                ),
            ),
        ),
    );

}

Finally, this is my simple bootstrap file:

<?php

/**
 * @var \MODX\Revolution\modX $modx
 * @var array $namespace
 */
$modx->addPackage('studioapp\Model', $namespace['path'] . 'src/', 'ods_', 'studioapp\\');

All good so far, but with all that, I can’t seem to pull any data from the package. I tried creating two different snippets to pull data:

#1

<?php
$output = "";

// Namespace
$namespace = "studioapp\\Model\\";

// Handle task generation

// Query for any lists
$activities = $modx->getCollection($namespace.'Activity', []);

if ($activities) {
    // Loop through them
    foreach ($activities as $activity) {
        // Add the list to the output
        $output .= PHP_EOL.PHP_EOL.'(' . $activity->get('id') . ') '.$activity->get('activity');
        }
    }
}

return $output;

#2

<?php
$base_path = $modx->getOption('core_path') . 'components/studioapp/src/Model/'; 
$modx->addPackage('studioapp', $base_path);

use studioapp\Activity;

$items = $modx->getCollection(Activity::class);

$output = [];
foreach ($items as $item) {
    $item_array = $item->toArray();
    $output[] = json_encode($item_array);
}
return implode("<br>", $output);

Either way I get the following in my error logs, which I assume its just a path issue but I can’t figure out where.

[2025-05-25 12:03:46] (WARN @ /www/core/vendor/xpdo/xpdo/src/xPDO/xPDO.php : 527) Could not load package metadata for package studioapp. Upgrade your model.
[2025-05-25 12:03:46] (ERROR @ /www/core/vendor/xpdo/xpdo/src/xPDO/xPDO.php : 667) Could not load class: studioapp\Activity from mysql.studioapp\dancers
[2025-05-25 12:03:46] (ERROR @ /www/core/vendor/xpdo/xpdo/src/xPDO/xPDO.php : 787) studioapp\Activity::loadCollection() is not a valid static method.

This is the first time I’ve tried to generate and load a class the modx 3 way. Can anyone with more experience see what I’m missing?

Not sure if this is the problem, but the core_path usually just points to {core_path}components/<package_name>/ (and not the src subfolder).

Good point, changed it - cleared my cache and still getting the same errors unfortunately.

Looks like it was an issue with my bootstrap file

<?php

/**
 * @var \MODX\Revolution\modX $modx
 * @var array $namespace
 */
$modx->addPackage('studioapp\Model', $namespace['path'] . 'src/', 'ods_', 'studioapp\\');

Instead of trying to use the namespace path, I used the core path and it found the package, but then it failed to find the class because it was looking for ods_ods_activity instead of just ods_activity so I made the table prefix simply ""

$modx->addPackage('studioapp\Model', $modx->getOption('core_path') . 'components/studioapp/src/', '', 'studioapp\\');

I don’t see why $namespace['path'] wouldn’t work. The same variable is used to locate your bootstrap.php file. So if your bootstrap.php runs, then the variable should have the correct value and $namespace['path'] . 'src/' should point to the src subfolder.


Also regarding your second snippet test code:
You don’t need to call addPackage() again. This already happens in the bootstrap.php. And the use statement is missing the \Model\ in the class name.

<?php
// $base_path = $modx->getOption('core_path') . 'components/studioapp/src/Model/'; 
// $modx->addPackage('studioapp', $base_path);

use studioapp\Model\Activity;

$items = $modx->getCollection(Activity::class);

I don’t see why $namespace['path'] wouldn’t work. The same variable is used to locate your bootstrap.php file. So if your bootstrap.php runs, then the variable should have the correct value and $namespace['path'] . 'src/' should point to the src subfolder.

Honestly, I have no idea why this worked but it’s the only thing I changed.

Also regarding your second snippet test code:
You don’t need to call addPackage() again. This already happens in the bootstrap.php . And the use statement is missing the \Model\ in the class name.

Yeah, I added it there because I was trying to see if the bootstrap file wasn’t running. After updating the bootstrap file, I tested and got the first snippet running and then I removed the second add package from the second snippet and did notice it was missing \Model\
so added that in as well.

// Add the package correctly

// Use the correct, full namespace
use studioapp\Model\Activity;

// Now you can use Activity::class
$items = $modx->getCollection(Activity::class);

$output = [];
foreach ($items as $item) {
    $item_array = $item->toArray();
    $output[] = json_encode($item_array);
}

return implode("<br>", $output);
1 Like

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”.