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?