Can xPDO gather objects linked by foreign keys?

Summary

Hello, I’m building an ecommerce app/plugin. I’ve got my database tables created and am now creating the foreign keys to related tables for product data.

I’m hoping to be able to use $xpdo->getObject(); to retrieve a product by automatically including all the foreign key assigned data tables of the product.

For example I have:
Product

  • ProductTranslation
  • ProductAssociation
  • ProductAttribute
  • ProductSource

I’m hoping to gather all that data by getting the Product object.

Is this something that’s possible outside of MODX native core objects?

Environment

MODX 2.8.1-pl
nginx 1.18.0
PHP 7.4.3
Ubuntu 20.04
mysql 8.0.23

Hi @NuzyWeb

That’s certainly possible, however with xPDO foreign keys aren’t used in the regular way. In your xPDO schema you would set composite and aggregate relationships as shown here in the docs:

If you’re able to share your schema so far, I’m sure myself and others can help you out.

Once you have relationships defined, you can use helper functions such as getOne() and getMany(), or if there’s not a relationship defined you can join tables explicitly.

MODX user data for example is mainly stored in two objects/tables, modUser and modUserProfile.
In the core schema they have a one-to-one relationship defined, and the relationship alias used for modUserProfile is defined as simply Profile.
So, if you want a user’s email, that’s in the profile, you would use getOne() to join it to the modUser object. Then you have access to data from both.

A basic function to do this in one of your own classes might be:

public function getUserEmail(int $userId) : string
{
    $user = $modx->getObject('modUser',$userId);
    $profile = $user->getOne('Profile');
    return $profile->get('email');
}
1 Like

So, composite is the targeted objects we want to collect, and aggregate is the object we want to collect them by?

In the store owners example I don’t see why you wouldn’t just do $getCollection(‘Owners’, array(‘store_id’ => $id));

I’m having trouble relating that scenario to collecting product data from 4 tables.

In your linked documentation It also shows using innerJoin preferred to getOne…so basically we don’t even need to add the composite or aggregate XML then if we’re just doing inner joins on multiple tables.

Ideally I would like to:
// Get the Product object by id
$product = $xpdo->getObject(‘Product’, array(‘key’ => ‘value’));

// Get data from 4 or more tables as aggregate data for the Product object by foreign key relations of the parenting id field in the Product object
$productData = $product->getOne(‘ProductData’);

The inner joins would defeat the purpose of getOne, wouldn’t they? I mean aren’t we trying to avoid using all the inner joins to aggregate the data of the product? Isn’t that the whole point of getOne?

The reason I asked here in the forum is because that store owners example in the documentation is only one dimensional and is illogical to me from every angle. If we have the store id and we know the owners table has a field store_id then we can iterate the result of $getCollection(‘Owners’, array(‘store_id’ => $id));. We don’t need getOne or innerJoin in that scenario.

Here’s my XML so far:

<?xml version="1.0" encoding="UTF-8"?>
<model package="GenericStore" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
	<object class="Product" table="product" extends="xPDOSimpleObject">
		<field key="main_taxon_id" dbtype="int" phptype="integer" null="true" index="index" />
		<field key="code" dbtype="varchar" precision="255" phptype="string" null="false" index="unique" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />
		<field key="enabled" dbtype="tinyint" precision="1" phptype="integer" null="false" />
		<field key="variant_selection_method" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="average_rating" dbtype="double" phptype="float" null="false" default="0" />

		<index alias="UNIQ_677B9B7477153098" name="UNIQ_677B9B7477153098" primary="false" unique="true" type="BTREE" >
			<column key="code" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_677B9B74731E505" name="IDX_677B9B74731E505" primary="false" unique="false" type="BTREE" >
			<column key="main_taxon_id" length="" collation="A" null="true" />
		</index>

		<aggregate alias="ProductAssociation" class="ProductAssociation" local="id" foreign="product_id" cardinality="one" owner="foreign" />
		<aggregate alias="ProductAttributeValue" class="ProductAttributeValue" local="id" foreign="product_id" cardinality="one" owner="foreign" />
		<aggregate alias="ProductChannels" class="ProductChannels" local="id" foreign="product_id" cardinality="one" owner="foreign" />
		<aggregate alias="ProductImage" class="ProductImage" local="id" foreign="owner_id" cardinality="one" owner="foreign" />
		<aggregate alias="ProductSourceCost" class="ProductSourceCost" local="id" foreign="product_id" cardinality="one" owner="foreign" />
		<aggregate alias="ProductTranslation" class="ProductTranslation" local="id" foreign="translatable_id" cardinality="one" owner="foreign" />
	</object>
	<object class="ProductAssociation" table="product_association" extends="xPDOSimpleObject">
		<field key="association_type_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />

		<index alias="product_association_idx" name="product_association_idx" primary="false" unique="true" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDABB1E1C39" name="IDX_48E9CDABB1E1C39" primary="false" unique="false" type="BTREE" >
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDAB4584665A" name="IDX_48E9CDAB4584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>

        <composite alias="Product" class="Product" local="product_id" foreign="id" cardinality="many" owner="local" />
	</object>
	<object class="ProductAttributeValue" table="product_attribute_value" extends="xPDOSimpleObject">
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="attribute_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="text_value" dbtype="longtext" phptype="string" null="true" />
		<field key="boolean_value" dbtype="tinyint" precision="1" phptype="integer" null="true" />
		<field key="integer_value" dbtype="int" phptype="integer" null="true" />
		<field key="float_value" dbtype="double" phptype="float" null="true" />
		<field key="datetime_value" dbtype="datetime" phptype="datetime" null="true" />
		<field key="date_value" dbtype="date" phptype="date" null="true" />
		<field key="json_value" dbtype="longtext" phptype="string" null="true" />
		<field key="locale_code" dbtype="varchar" precision="255" phptype="string" null="false" />

		<index alias="IDX_8A053E544584665A" name="IDX_8A053E544584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_8A053E54B6E62EFA" name="IDX_8A053E54B6E62EFA" primary="false" unique="false" type="BTREE" >
			<column key="attribute_id" length="" collation="A" null="false" />
		</index>

        <composite alias="Product" class="Product" local="product_id" foreign="id" cardinality="many" owner="local" />
	</object>
	<object class="ProductChannels" table="product_channels" extends="xPDOObject">
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="pk" />
		<field key="channel_id" dbtype="int" phptype="integer" null="false" index="pk" />

		<index alias="PRIMARY" name="PRIMARY" primary="true" unique="true" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
			<column key="channel_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_F9EF269B4584665A" name="IDX_F9EF269B4584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_F9EF269B72F5A1AA" name="IDX_F9EF269B72F5A1AA" primary="false" unique="false" type="BTREE" >
			<column key="channel_id" length="" collation="A" null="false" />
		</index>

        <composite alias="Product" class="Product" local="product_id" foreign="id" cardinality="many" owner="local" />
	</object>
	<object class="ProductImage" table="product_image" extends="xPDOSimpleObject">
		<field key="owner_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="type" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="path" dbtype="varchar" precision="255" phptype="string" null="false" />

		<index alias="IDX_88C64B2D7E3C61F9" name="IDX_88C64B2D7E3C61F9" primary="false" unique="false" type="BTREE" >
			<column key="owner_id" length="" collation="A" null="false" />
		</index>

        <composite alias="Product" class="Product" local="owner_id" foreign="id" cardinality="many" owner="local" />
	</object>
	<object class="ProductSourceCost" table="product_source_cost" extends="xPDOSimpleObject">
		<field key="source_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="cost" dbtype="int" phptype="integer" null="false" />
		<field key="currency_id" dbtype="int" phptype="integer" null="false" index="index" />

		<index alias="vdx_product_source_cost_currency_idx" name="vdx_product_source_cost_currency_idx" primary="false" unique="true" type="BTREE" >
			<column key="currency_id" length="" collation="A" null="false" />
			<column key="source_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_9231DCA2C2AC5D3" name="IDX_9231DCA2C2AC5D3" primary="false" unique="false" type="BTREE" >
			<column key="source_id" length="" collation="A" null="false" />
		</index>
		<index alias="vdx_product_source_cost_product_idx" name="vdx_product_source_cost_product_idx" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>

        <composite alias="Product" class="Product" local="product_id" foreign="id" cardinality="many" owner="local" />
	</object>
	<object class="ProductTranslation" table="product_translation" extends="xPDOSimpleObject">
		<field key="translatable_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="slug" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="description" dbtype="longtext" phptype="string" null="true" />
		<field key="meta_keywords" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="meta_description" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="short_description" dbtype="longtext" phptype="string" null="true" />
		<field key="locale" dbtype="varchar" precision="255" phptype="string" null="false" index="index" />

		<index alias="vdx_product_translation_uniq_trans" name="vdx_product_translation_uniq_trans" primary="false" unique="true" type="BTREE" >
			<column key="translatable_id" length="" collation="A" null="false" />
			<column key="locale" length="" collation="A" null="false" />
		</index>
		<index alias="UNIQ_105A9084180C698989D9B62" name="UNIQ_105A9084180C698989D9B62" primary="false" unique="true" type="BTREE" >
			<column key="locale" length="" collation="A" null="false" />
			<column key="slug" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_105A9082C2AC5D3" name="IDX_105A9082C2AC5D3" primary="false" unique="false" type="BTREE" >
			<column key="translatable_id" length="" collation="A" null="false" />
		</index>
        
        <composite alias="Product" class="Product" local="translatable_id" foreign="id" cardinality="many" owner="local" />
	</object>
</model>

As long as I’m on the right track here I can setup the next ~10 relations for the other composites I need to aggregate.

Doing all this XML will allow me to use the getOne() method on my $product object to collect all these composites, right?

I believe your schema is all backwards.

<model package="GenericStore" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
	<object class="Product" table="product" extends="xPDOSimpleObject">
		...
		<aggregate alias="ProductImage" class="ProductImage" local="id" foreign="owner_id" cardinality="one" owner="foreign" />
	</object>
	<object class="ProductImage" table="product_image" extends="xPDOSimpleObject">
		<field key="owner_id" dbtype="int" phptype="integer" null="false" index="index" />
		...
        <composite alias="Product" class="Product" local="owner_id" foreign="id" cardinality="many" owner="local" />
	</object>
</model>

This schema means:

  • A product has 1 image.
  • An image belongs to many products.
  • If I delete an image, the associated products should be deleted too.
  • If I delete a product, the associated image shouldn’t be deleted.

Is that what you want?


The more logical way is to have a schema like this:

<model package="GenericStore" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
	<object class="Product" table="product" extends="xPDOSimpleObject">
		...
		<composite alias="ProductImages" class="ProductImage" local="id" foreign="owner_id" cardinality="many" owner="local" />
	</object>
	<object class="ProductImage" table="product_image" extends="xPDOSimpleObject">
		<field key="owner_id" dbtype="int" phptype="integer" null="false" index="index" />
		...
		<aggregate alias="Product" class="Product" local="owner_id" foreign="id" cardinality="one" owner="foreign" />
	</object>
</model>

This schema means:

  • A product has many images.
  • An image belongs to 1 product.
  • If I delete a product, the associated images should be deleted too.
  • If I delete an image, the associated product shouldn’t be deleted.

With his schema, if you have a product

$product = $xpdo->getObject('Product', array('key' => 'value'));

you can then read all the images with

$productImages = $product->getMany('ProductImages');

which is equivalent to

$productImages = $xpdo->getCollection('ProductImage', array('owner_id' => $product->get('id')));
1 Like

Hello, @halftrainedharry. I see what you’re saying here. The the aggregate is the child to composite.

That clears things up a bit.

In my case there is actually only one image for each product class entry (default image), then there are multiple variant images for each possible variant of a product, but I do want the behavior you describe as logical.

I only have ~90 more objects to associate wit their parent now. Haha.

Thank you @digitalpenguin, and @halftrainedharry for the help.

Now that I’ve got my schema setup, how do I get access to the getOne() method?

My class files are all generated and I can see them in core/components/MyPackage/model/MyPackage.

Isn’t getOne() part of the extension of xPDO class or do I need to write it into the class file for the Product object?

I’m getting this error:

Fatal error: Uncaught Error: Call to undefined method xPDO::getOne()

Here’s my updated XML:

<?xml version="1.0" encoding="UTF-8"?>
	<object class="Product" table="product" extends="xPDOSimpleObject">
		<field key="main_taxon_id" dbtype="int" phptype="integer" null="true" index="index" />
		<field key="code" dbtype="varchar" precision="255" phptype="string" null="false" index="unique" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />
		<field key="enabled" dbtype="tinyint" precision="1" phptype="integer" null="false" />
		<field key="variant_selection_method" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="average_rating" dbtype="double" phptype="float" null="false" default="0" />

		<index alias="UNIQ_677B9B7477153098" name="UNIQ_677B9B7477153098" primary="false" unique="true" type="BTREE" >
			<column key="code" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_677B9B74731E505" name="IDX_677B9B74731E505" primary="false" unique="false" type="BTREE" >
			<column key="main_taxon_id" length="" collation="A" null="true" />
		</index>

		<composite alias="ProductData" class="ProductAssociation" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="ProductData" class="ProductAttributeValue" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="ProductData" class="ProductChannels" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="ProductData" class="ProductImage" local="id" foreign="owner_id" cardinality="many" owner="local" />
		<composite alias="ProductData" class="ProductSourceCost" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="ProductData" class="ProductTranslation" local="id" foreign="translatable_id" cardinality="many" owner="local" />
	</object>

	
	<object class="ProductAssociation" table="product_association" extends="xPDOSimpleObject">
		<field key="association_type_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />

		<index alias="product_association_idx" name="product_association_idx" primary="false" unique="true" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDABB1E1C39" name="IDX_48E9CDABB1E1C39" primary="false" unique="false" type="BTREE" >
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDAB4584665A" name="IDX_48E9CDAB4584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="product_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

	
	<object class="ProductAttributeValue" table="product_attribute_value" extends="xPDOSimpleObject">
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="attribute_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="text_value" dbtype="longtext" phptype="string" null="true" />
		<field key="boolean_value" dbtype="tinyint" precision="1" phptype="integer" null="true" />
		<field key="integer_value" dbtype="int" phptype="integer" null="true" />
		<field key="float_value" dbtype="double" phptype="float" null="true" />
		<field key="datetime_value" dbtype="datetime" phptype="datetime" null="true" />
		<field key="date_value" dbtype="date" phptype="date" null="true" />
		<field key="json_value" dbtype="longtext" phptype="string" null="true" />
		<field key="locale_code" dbtype="varchar" precision="255" phptype="string" null="false" />

		<index alias="IDX_8A053E544584665A" name="IDX_8A053E544584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_8A053E54B6E62EFA" name="IDX_8A053E54B6E62EFA" primary="false" unique="false" type="BTREE" >
			<column key="attribute_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="product_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

	
	<object class="ProductChannels" table="product_channels" extends="xPDOObject">
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="pk" />
		<field key="channel_id" dbtype="int" phptype="integer" null="false" index="pk" />

		<index alias="PRIMARY" name="PRIMARY" primary="true" unique="true" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
			<column key="channel_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_F9EF269B4584665A" name="IDX_F9EF269B4584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_F9EF269B72F5A1AA" name="IDX_F9EF269B72F5A1AA" primary="false" unique="false" type="BTREE" >
			<column key="channel_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="product_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

	
	<object class="ProductImage" table="product_image" extends="xPDOSimpleObject">
		<field key="owner_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="type" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="path" dbtype="varchar" precision="255" phptype="string" null="false" />

		<index alias="IDX_88C64B2D7E3C61F9" name="IDX_88C64B2D7E3C61F9" primary="false" unique="false" type="BTREE" >
			<column key="owner_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="owner_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

	
	<object class="ProductSourceCost" table="product_source_cost" extends="xPDOSimpleObject">
		<field key="source_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="cost" dbtype="int" phptype="integer" null="false" />
		<field key="currency_id" dbtype="int" phptype="integer" null="false" index="index" />

		<index alias="vdx_product_source_cost_currency_idx" name="vdx_product_source_cost_currency_idx" primary="false" unique="true" type="BTREE" >
			<column key="currency_id" length="" collation="A" null="false" />
			<column key="source_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_9231DCA2C2AC5D3" name="IDX_9231DCA2C2AC5D3" primary="false" unique="false" type="BTREE" >
			<column key="source_id" length="" collation="A" null="false" />
		</index>
		<index alias="vdx_product_source_cost_product_idx" name="vdx_product_source_cost_product_idx" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="product_id" foreign="id" cardinality="one" owner="foreign" />
	</object>

	
	<object class="ProductTranslation" table="product_translation" extends="xPDOSimpleObject">
		<field key="translatable_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="slug" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="description" dbtype="longtext" phptype="string" null="true" />
		<field key="meta_keywords" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="meta_description" dbtype="varchar" precision="255" phptype="string" null="true" />
		<field key="short_description" dbtype="longtext" phptype="string" null="true" />
		<field key="locale" dbtype="varchar" precision="255" phptype="string" null="false" index="index" />

		<index alias="vdx_product_translation_uniq_trans" name="vdx_product_translation_uniq_trans" primary="false" unique="true" type="BTREE" >
			<column key="translatable_id" length="" collation="A" null="false" />
			<column key="locale" length="" collation="A" null="false" />
		</index>
		<index alias="UNIQ_105A9084180C698989D9B62" name="UNIQ_105A9084180C698989D9B62" primary="false" unique="true" type="BTREE" >
			<column key="locale" length="" collation="A" null="false" />
			<column key="slug" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_105A9082C2AC5D3" name="IDX_105A9082C2AC5D3" primary="false" unique="false" type="BTREE" >
			<column key="translatable_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="ProductData" class="Product" local="translatable_id" foreign="id" cardinality="one" owner="local" />
	</object>
</model>

Have you made an addPackage() call?
You’ll need this to load the custom model.

Also, have you had a look at the Commerce extra? It could save you a lot of development time.

I actually am calling addPackage, @digitalpenguin.

Here’s my testing snippet:

// Package vars
$package    = 'MyPackage';
$prefix     = 'pre_';


// xPDO version
include MODX_CORE_PATH.'config/config.inc.php';
$xpdo = new xPDO($database_dsn, $database_user, $database_password, $table_prefix);

$xpdo->setLogLevel(xPDO::LOG_LEVEL_INFO);
$xpdo->setLogTarget('ECHO');

if (!$xpdo->addPackage($package, MODX_CORE_PATH."components/$package/model/", $prefix)) {
    print 'There was a problem adding your package.';
} else {
    $productTrans   = $xpdo->getObject('ProductTranslation', array('slug' => 'test-product') );
    $product        = $xpdo->getObject('Product', array('id' => $productTrans->get('translatable_id')) );
    $productData    = $product->getOne('ProductData');

    echo '<pre>';
    print_r($productData);
    echo '</pre>';

}

I’m able to print_r($product), so I know that it’s able tp access the package and retrieve the Product object, but when it hits the next line with the getOne() call it fails with the error that getOne().

New error:

No foreign key definition for parentClass: Product using relation alias: ProductData
Could not getOne: foreign key definition for alias ProductData not found.

I just cleared the cache multiple times and it’s still telling me that same relation alias error.

Is there something I need to do in the system settings or another XML tag I need?

Your relations in the XML you posted are not correct. The alias must be unique for each relation you define.

Hi @opengeek. That error makes sense now.

I was hoping I didn’t need to write a new method to build the ProductData object. Looks like that’s gonna be the only way to handle this though.

Thank you guys again for all the help.

For your xPDO queries, you might look at $modx->getObjectGraph(), and $modx->getCollectionGraph().

The first one gets an object and it’s related objects in one query. The second gets a collection of objects and their related objects in one query. They both take the same arguments.

When I was trying to understand this stuff, I found looking at the modUser object in the MODX schema was really helpful (lines 1238-1277):

You can see that the aggregate aliases are all for objects that you wouldn’t want to remove when a user is deleted.

The composite aliases are all for objects that should be removed when the user is deleted.

I think your composites should look like that:

		<composite alias="Associations" class="ProductAssociation" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="AttributeValues" class="ProductAttributeValue" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="Channels" class="ProductChannels" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="Images" class="ProductImage" local="id" foreign="owner_id" cardinality="many" owner="local" />
		<composite alias="Costs" class="ProductSourceCost" local="id" foreign="product_id" cardinality="many" owner="local" />
		<composite alias="Translations" class="ProductTranslation" local="id" foreign="translatable_id" cardinality="many" owner="local" />

and you can’t get them all within one query, you would need to get the related items for one product with multiple queries.

you could use getMany for each of the composites, since this all are one2many relations

Hi @bobray. I just tried getObjectGraph, and I still get the No foreign key definition for parentClass: Product using relation alias: Association error.

Here’s what I’m using now:

Snippet

<?php
// Package vars
$package    = 'Vendx';
$prefix     = 'vdx_';


// xPDO
include MODX_CORE_PATH.'config/config.inc.php';
$xpdo = new xPDO($database_dsn, $database_user, $database_password, $table_prefix);

$xpdo->setLogLevel(xPDO::LOG_LEVEL_INFO);
$xpdo->setLogTarget('ECHO');

if (!$xpdo->addPackage($package, MODX_CORE_PATH."components/$package/model/", $prefix)) {
    print 'There was a problem adding your package.';
} else {
    $productTrans   = $xpdo->getObject('ProductTranslation', array('slug' => 'test-product') );
    $pid            = $productTrans->get('translatable_id');
    $product        = $xpdo->getCollectionGraph('Product', array('Association' => array('product_id' => $pid)), $pid);

    echo '<pre>';
    print_r($product);
    echo '</pre>';

}
?>

XML

<?xml version="1.0" encoding="UTF-8"?>
<model package="Vendx" baseClass="xPDOObject" platform="mysql" defaultEngine="InnoDB" version="1.1">
	<object class="Product" table="product" extends="xPDOSimpleObject">
		<field key="main_taxon_id" dbtype="int" phptype="integer" null="true" index="index" />
		<field key="code" dbtype="varchar" precision="255" phptype="string" null="false" index="unique" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />
		<field key="enabled" dbtype="tinyint" precision="1" phptype="integer" null="false" />
		<field key="variant_selection_method" dbtype="varchar" precision="255" phptype="string" null="false" />
		<field key="average_rating" dbtype="double" phptype="float" null="false" default="0" />

		<index alias="UNIQ_677B9B7477153098" name="UNIQ_677B9B7477153098" primary="false" unique="true" type="BTREE" >
			<column key="code" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_677B9B74731E505" name="IDX_677B9B74731E505" primary="false" unique="false" type="BTREE" >
			<column key="main_taxon_id" length="" collation="A" null="true" />
		</index>

        <composite alias="Association" class="ProductAssociation" local="id" foreign="product_id" cardinality="many" owner="local" />
	</object>

	
	<object class="ProductAssociation" table="product_association" extends="xPDOSimpleObject">
		<field key="association_type_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="product_id" dbtype="int" phptype="integer" null="false" index="index" />
		<field key="created_at" dbtype="datetime" phptype="datetime" null="false" />
		<field key="updated_at" dbtype="datetime" phptype="datetime" null="true" />

		<index alias="product_association_idx" name="product_association_idx" primary="false" unique="true" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDABB1E1C39" name="IDX_48E9CDABB1E1C39" primary="false" unique="false" type="BTREE" >
			<column key="association_type_id" length="" collation="A" null="false" />
		</index>
		<index alias="IDX_48E9CDAB4584665A" name="IDX_48E9CDAB4584665A" primary="false" unique="false" type="BTREE" >
			<column key="product_id" length="" collation="A" null="false" />
		</index>

        <aggregate alias="Product" class="Product" local="product_id" foreign="id" cardinality="one" owner="foreign" />
	</object>
</model>

Even with this simplified to the above code I still get that foreign key error when using any call to getOne, getMany, getObjectGraph, or getCollectionGraph, and their respective key => value array pointers.

The Product object does get retrieved, just not getting any relational data with it.

I realized I was previously missing the opening <model> element in the XML, but have added it as shown above before producing the noted error. I’ve cleared the cache to be sure.

The only thing that works without producing the foreign key relation error is getObject, or getCollection.

This foreign key exists in the MySQL database but has a constraint name of vdx_product_association_ibfk_1. Should that be changed to the alias Association like in the XML file?

Is there some other place I need to tell MODX that I’m using these foreign keys?

Hi @bruno17, I did change to almost exactly what you posted and all the aliases errored with No foreign key definition for parentClass: Product using relation alias: RespectiveAliasUsed.

For some reason I think that the XML file isn’t being read correctly, or I have a typo in it?

Clearing cache multiple times still produces the error as if those aggregate and composite statements don’t exist.

something like this should work:

    $productTrans   = $xpdo->getObject('ProductTranslation', array('slug' => 'test-product') );
    $product = $productTrans->getOne('ProductData');
    $associations = $product->getMany('Associations');

did you parse the schema, after changing it, again?

I just did this:

$manager = $xpdo->getManager();
$generator = $manager->getGenerator();
$generator->parseSchema('Vendx.mysql.schema.xml', MODX_CORE_PATH."components/$package/model/");

Edit: $package is defined in variable a few line above it. No errors seen when running parseSchema.

And cleared the cache multiple times.

Still getting that same error.

Edit 2: Also getting an empty array returned.

Current snippet code:

<?php
// Package vars
$package    = 'Vendx';
$prefix     = 'vdx_';


// xPDO
include MODX_CORE_PATH.'config/config.inc.php';
$xpdo = new xPDO($database_dsn, $database_user, $database_password, $table_prefix);

//$manager = $xpdo->getManager();
//$generator = $manager->getGenerator();
//$generator->parseSchema('Vendx.mysql.schema.xml', MODX_CORE_PATH."components/$package/model/");


$xpdo->setLogLevel(xPDO::LOG_LEVEL_INFO);
$xpdo->setLogTarget('ECHO');

if (!$xpdo->addPackage($package, MODX_CORE_PATH."components/$package/model/", $prefix)) {
    print 'There was a problem adding your package.';
} else {
    $productTrans   = $xpdo->getObject('ProductTranslation', array('slug' => 'test-product') );
    $pid            = $productTrans->get('translatable_id');
    $product        = $xpdo->getObject('Product', $pid);
    $productAssoc   = $product->getMany( 'Association' );

    echo '<pre>';
    print_r($productAssoc);
    echo '</pre>';

}

the same error with my posted code?
did you check, if the aliases a correct, which I wrote?

maybe you have Association in your schema, but I wrote Associations

In my XML there is no “s” at the end of “Association”.

In you simplyfied schema, you don’t have ProductTranslation defined, but you are trying to get that object