Skip to content
  1. Extras
  2. MiniShop3
  3. Development
  4. Models and database schema

Models and database schema

MiniShop3 uses xPDO ORM for database access. All models live in the MiniShop3\Model namespace.

xPDO models

Core classes

php
use MiniShop3\Model\msProduct;
use MiniShop3\Model\msProductData;
use MiniShop3\Model\msOrder;
use MiniShop3\Model\msCustomer;

// Get product
$product = $modx->getObject(msProduct::class, $id);

// Get product data
$data = $product->getOne('Data');

// Get order with address
$order = $modx->getObject(msOrder::class, $id);
$address = $order->getOne('Address');

// Get customer
$customer = $modx->getObject(msCustomer::class, ['email' => $email]);

Model relations

php
// Product → Product data (1:1)
$product->getOne('Data');

// Product → Options (1:N)
$product->getMany('Options');

// Product → Gallery files (1:N)
$data->getMany('Files');

// Order → Order products (1:N)
$order->getMany('Products');

// Order → Customer (N:1)
$order->getOne('msCustomer');

// Customer → Addresses (1:N)
$customer->getMany('Addresses');

Schema file

The database schema is defined in an XML file:

core/components/minishop3/schema/minishop3.mysql.schema.xml

Main schema attributes:

  • package: MiniShop3\Model
  • baseClass: xPDO\Om\xPDOObject
  • platform: mysql
  • version: 3.0

Database tables

Products

ModelTableDescription
msProductsite_contentProduct (extends modResource)
msProductDatams3_productsProduct data (price, article, weight)
msProductFilems3_product_filesProduct gallery files
msProductOptionms3_product_optionsProduct option values
msLinkms3_linksProduct link types
msProductLinkms3_product_linksProduct-to-product links

msProductData — main fields

FieldTypeDescription
idintID (matches resource ID)
articlevarchar(50)Article/SKU
pricedecimal(12,2)Price
old_pricedecimal(12,2)Old price
stockdecimal(13,3)Stock quantity
weightdecimal(13,3)Weight
imagevarchar(255)Main image
thumbvarchar(255)Thumbnail
vendor_idintVendor ID
made_invarchar(100)Country of origin
newtinyint(1)"New" flag
populartinyint(1)"Popular" flag
favoritetinyint(1)"Favorite" flag
tagsjsonTags
colorjsonColors
sizejsonSizes

Categories

ModelTableDescription
msCategorysite_contentCategory (extends modResource)
msCategoryMemberms3_product_categoriesProduct–category relation
msCategoryOptionms3_category_optionsCategory options

Orders

ModelTableDescription
msOrderms3_ordersOrder
msOrderAddressms3_order_addressesOrder delivery address
msOrderProductms3_order_productsOrder line items
msOrderLogms3_order_logsOrder change history
msOrderStatusms3_order_statusesOrder statuses

msOrder — main fields

FieldTypeDescription
idintOrder ID
user_idintMODX user ID
customer_idintmsCustomer ID
tokenvarchar(128)Session token
uuidchar(36)Order UUID
numvarchar(20)Order number
costdecimal(12,2)Total cost
cart_costdecimal(12,2)Products cost
delivery_costdecimal(12,2)Delivery cost
weightdecimal(13,3)Total weight
status_idintStatus ID
delivery_idintDelivery method ID
payment_idintPayment method ID
contextvarchar(100)MODX context
order_commenttextOrder comment
createdondatetimeCreated at
updatedondatetimeUpdated at

Customers (NEW in MiniShop3)

ModelTableDescription
msCustomerms3_customersStore customer
msCustomerAddressms3_customer_addressesSaved customer addresses
msCustomerTokenms3_customer_tokensAuth tokens

msCustomer — main fields

FieldTypeDescription
idintCustomer ID
user_idintLinked modUser ID (optional)
first_namevarchar(191)First name
last_namevarchar(191)Last name
phonevarchar(50)Phone
emailvarchar(191)Email
passwordvarchar(255)Password hash
is_activetinyint(1)Active
is_blockedtinyint(1)Blocked
email_verified_atdatetimeEmail verification date
orders_countintOrder count
total_spentdecimal(12,2)Total order sum
last_order_atdatetimeLast order date
privacy_accepted_atdatetimePrivacy policy acceptance date

Difference from miniShop2

In miniShop2 customer = modUser. In MiniShop3 customer is a separate entity msCustomer, which can optionally be linked to modUser.

Delivery and payment

ModelTableDescription
msDeliveryms3_deliveriesDelivery methods
msPaymentms3_paymentsPayment methods
msDeliveryMemberms3_delivery_paymentsDelivery–payment link

Vendors

ModelTableDescription
msVendorms3_vendorsProduct vendors

Options

ModelTableDescription
msOptionms3_optionsOption catalog

Field configuration (NEW in MiniShop3)

ModelTableDescription
msModelFieldms3_model_fieldsModel field settings
msModelFieldSectionms3_model_field_sectionsField sections
msProductFieldms3_product_fieldsProduct fields (legacy)
msPageSectionms3_page_sectionsPage sections (legacy)
msExtraFieldms3_extra_fieldsExtra fields

Notifications

ModelTableDescription
msNotificationConfigms3_notification_configsNotification configuration

Working with models

Creating an order

php
use MiniShop3\Model\msOrder;
use MiniShop3\Model\msOrderProduct;
use MiniShop3\Model\msOrderAddress;

// Create order
$order = $modx->newObject(msOrder::class);
$order->fromArray([
    'customer_id' => $customerId,
    'user_id' => $modx->user->get('id') ?: 0,
    'status_id' => $modx->getOption('ms3_status_new'),
    'delivery_id' => $deliveryId,
    'payment_id' => $paymentId,
    'context' => $modx->context->key,
]);
$order->save();

// Add product to order
$orderProduct = $modx->newObject(msOrderProduct::class);
$orderProduct->fromArray([
    'order_id' => $order->get('id'),
    'product_id' => $productId,
    'name' => $productName,
    'price' => $price,
    'count' => $count,
    'cost' => $price * $count,
    'options' => json_encode($options),
]);
$orderProduct->save();

Working with customer

php
use MiniShop3\Model\msCustomer;
use MiniShop3\Model\msCustomerAddress;

// Find or create customer
$customer = $modx->getObject(msCustomer::class, ['email' => $email]);
if (!$customer) {
    $customer = $modx->newObject(msCustomer::class);
    $customer->fromArray([
        'email' => $email,
        'first_name' => $firstName,
        'phone' => $phone,
    ]);
    $customer->save();
}

// Add address
$address = $modx->newObject(msCustomerAddress::class);
$address->fromArray([
    'customer_id' => $customer->get('id'),
    'city' => $city,
    'street' => $street,
    'building' => $building,
]);
$address->save();

// Get all customer addresses
$addresses = $customer->getMany('Addresses');

Iterating records

php
use MiniShop3\Model\msOrder;

// Correct: getIterator() — lazy loading
foreach ($modx->getIterator(msOrder::class, ['status_id' => 2]) as $order) {
    // Process order
}

// Incorrect: getCollection() loads everything into memory
// $orders = $modx->getCollection(msOrder::class, $criteria);

Model architecture

Difference from miniShop2

miniShop2 used "fat models" — model classes contained a lot of business logic: validation, calculations, notifications, etc.

In MiniShop3 business logic lives in the service layer (src/Services/, src/Controllers/):

miniShop2:
┌─────────────────────────────────┐
│           msOrder               │
│  - validation                   │
│  - cost calculation             │
│  - status change                │
│  - notifications                │
│  - database access              │
└─────────────────────────────────┘

MiniShop3:
┌─────────────────┐     ┌─────────────────────┐
│    msOrder      │◄────│  OrderController    │
│  - database     │     │  - business logic   │
│  - relations    │     └─────────────────────┘
│  - service      │     ┌─────────────────────┐
│    calls        │◄────│  NotificationManager│
└─────────────────┘     │  - notifications    │
                        └─────────────────────┘

Pragmatic approach

Compromise with MODX architecture

In modern approaches (Clean Architecture, DDD) models should only handle database access and not know about the service layer, repositories, or controllers.

However, MODX architecture commonly puts business logic in models (modResource, modUser, etc.). Since some MiniShop3 models extend MODX classes (msProduct extends modResource, msCategory extends modResource), they must follow similar patterns.

Therefore MiniShop3 models reference services for business logic — a pragmatic compromise for MODX ecosystem compatibility.

Example: calling a service from a model

php
// src/Model/msProductData.php
class msProductData extends xPDOSimpleObject
{
    public function getPrice(array $data = []): float
    {
        // Delegate to formatting service
        $ms3 = $this->xpdo->services->get('ms3');
        return $ms3->format->price($this->get('price'));
    }
}

Regenerating models

Models are maintained manually

In MiniShop3 models are NOT auto-generated from the XML schema via buildModel(). They are created and maintained manually in src/Model/.

This allows:

  • Custom methods in models
  • Full PHP 8 type hints
  • Full control over model code

Important: Do not run buildModel() — it will overwrite custom model methods.

Model file structure

src/Model/
├── msProduct.php           # Main model class
├── mysql/
│   └── msProduct.php       # MySQL-specific mapping

Example model

php
<?php
namespace MiniShop3\Model;

use MODX\Revolution\modResource;

class msProduct extends modResource
{
    // Custom model methods
    public function getPrice(): float
    {
        $data = $this->getOne('Data');
        return $data ? (float)$data->get('price') : 0.0;
    }
}

Example mapping

php
<?php
namespace MiniShop3\Model\mysql;

class msProduct extends \MiniShop3\Model\msProduct
{
    public static $metaMap = [
        'package' => 'MiniShop3\\Model\\',
        'version' => '3.0',
        'extends' => 'MODX\\Revolution\\modResource',
        'fields' => [
            'class_key' => 'MiniShop3\\Model\\msProduct',
        ],
        // ... other metadata
    ];
}

Phinx migrations

Schema changes are applied via Phinx migrations:

core/components/minishop3/migrations/
├── 20240101000000_create_customers_table.php
├── 20240102000000_add_customer_fields.php
└── ...

Running migrations

bash
cd /path/to/modx
php vendor/bin/phinx migrate -c core/components/minishop3/phinx.php

Example migration

php
<?php
use Phinx\Migration\AbstractMigration;

class CreateCustomersTable extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('ms3_customers', [
            'engine' => 'InnoDB',
            'collation' => 'utf8mb4_unicode_ci',
        ]);

        $table
            ->addColumn('email', 'string', ['limit' => 191])
            ->addColumn('first_name', 'string', ['limit' => 191, 'null' => true])
            ->addColumn('phone', 'string', ['limit' => 50, 'null' => true])
            ->addIndex(['email'], ['unique' => true])
            ->create();
    }
}

Relation diagram

┌─────────────┐       ┌─────────────────┐
│  msProduct  │──1:1──│  msProductData  │
│ (modResource)│       │  (ms3_products) │
└──────┬──────┘       └────────┬────────┘
       │                       │
       │ 1:N                   │ 1:N
       ▼                       ▼
┌──────────────┐       ┌────────────────┐
│msProductOption│       │ msProductFile  │
└──────────────┘       └────────────────┘

┌─────────────┐       ┌─────────────────┐
│   msOrder   │──1:1──│ msOrderAddress  │
└──────┬──────┘       └─────────────────┘

       ├──1:N──▶ msOrderProduct

       ├──N:1──▶ msCustomer ──1:N──▶ msCustomerAddress

       ├──N:1──▶ msDelivery

       └──N:1──▶ msPayment