From redaxo-yform
Enables ORM-style read/write access to YForm tables in PHP via rex_yform_manager_dataset and rex_yform_manager_query. Supports model classes, where/join/select/order/group, relations, pagination, CRUD, and SQL debugging for custom lists and templates.
npx claudepluginhub friendsofredaxo/claude-marketplace --plugin redaxo-yformThis skill uses the workspace's default tool permissions.
`rex_yform_manager_dataset` is YForm's active-record-style ORM. One subclass per table, automatic loading/saving, lazy relations, fluent query builder.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Builds production-ready Apache Airflow DAGs with patterns for operators, sensors, testing, and deployment. For data pipelines, workflow orchestration, and batch jobs.
Share bugs, ideas, or general feedback.
rex_yform_manager_dataset is YForm's active-record-style ORM. One subclass per table, automatic loading/saving, lazy relations, fluent query builder.
$items = rex_yform_manager_table::get('rex_my_table')->query()->find();
// lib/MyTable.php
class MyTable extends rex_yform_manager_dataset {}
// boot.php
rex_yform_manager_dataset::setModelClass('rex_my_table', MyTable::class);
// Anywhere
$items = MyTable::query()->find();
After setModelClass(), queries on the table return your subclass instances.
class team_member extends rex_yform_manager_dataset {
public function getName(): string {
return (string) $this->getValue('name');
}
public function getPhotoMedia(): ?rex_media {
$filename = (string) $this->getValue('photo');
return $filename ? rex_media::get($filename) : null;
}
public function getRole(): ?team_role {
$relation = $this->getRelatedDataset('role');
return $relation instanceof team_role ? $relation : null;
}
}
$query = MyTable::query();
$query
->alias('t')
->joinRelation('category_id', 'c')
->select('c.name', 'category_name')
->where('t.status', '1')
->where('t.created', $date, '>')
->orderBy('t.name')
->limit(0, 10);
$items = $query->find();
Where: where, whereNot, whereNull, whereNotNull, whereBetween, whereNotBetween, whereNested, whereRaw, whereListContains, setWhereOperator, resetWhere
Having: having, havingNot, havingNull, havingNotNull, havingBetween, havingNotBetween, havingRaw, havingListContains, setHavingOperator, resetHaving
Join: joinRelation, leftJoinRelation, joinRaw, joinType, joinTypeRelation, leftJoin, resetJoins
Select: select, selectRaw, resetSelect
Order: orderBy, orderByRaw, resetOrderBy
Group: groupBy, groupByRaw, resetGroupBy
Other: alias, limit, resetLimit, count, exists, find, findOne, findId, findIds, paginate, save, getQuery, getParams
// Equality / comparison
->where('status', 1)
->where('age', 18, '>=')
->where('email', '%@example.com', 'LIKE')
// IN clause
->where('status', [1, 2])
// Raw SQL (for things the builder can't express)
->whereRaw('LOWER(name) = LOWER(?)', ['Alice'])
// List contains (for comma-separated values stored in a column)
->whereListContains('tags', 5)
->whereListContains('tags', [3, 5, 9])
// OR conditions
$query->setWhereOperator('OR')
->where('foo', 1)
->where('bar', 2);
$query->whereRaw('(foo = :foo OR bar = :bar)', ['foo' => 1, 'bar' => 2]);
// OR with whereNested (array form)
$query->whereNested(['foo' => 1, 'bar' => 2], 'OR');
// OR with whereNested (callback form)
$query->whereNested(function (rex_yform_manager_query $query) {
$query->where('foo', 1)->where('bar', 2);
}, 'OR');
$pager = new rex_pager(20); // 20 items per page
$items = MyTable::query()->where('status', 1)->paginate($pager);
$fragment = new rex_fragment();
$fragment->setVar('urlprovider', rex_article::getCurrent());
$fragment->setVar('pager', $pager);
echo $fragment->parse('core/navigations/pagination.php');
foreach ($items as $item) {
echo $item->getValue('name');
}
// Pager info
$pager->getRowCount();
$pager->getCurrentPage();
$pager->getLastPage();
$pager->getPageCount();
$member = MyTable::get(42); // by primary key – nullable
$member = MyTable::query()->where('email', $email)->findOne();
// Create
$item = rex_yform_manager_dataset::create('rex_my_table');
// or with model class: $item = MyTable::create();
$item->setValue('name', 'Test');
$item->save();
// Read
$item = rex_yform_manager_dataset::get($id, 'rex_my_table');
$item = MyTable::get($id);
// Update
$item = MyTable::get($id);
$item->name = 'New Name';
$item->save();
// Delete
$item = MyTable::get($id);
$item->delete();
// Bulk delete via collection
MyTable::query()->where('status', 0)->find()->delete();
save() runs the table's configured validators automatically. Failures populate getMessages(). Always check the return value or log messages.
// "to-one" (e.g. author of a post)
$author = $post->getRelatedDataset('author_id'); // dataset or null
// "to-many" (e.g. all posts of an author)
$posts = $author->getRelatedCollection('posts'); // collection
// Join in a query
$query = MyTable::query()
->joinRelation('author_id', 'a')
->selectRaw('CONCAT(a.first_name, " ", a.last_name)', 'author_name');
The string passed to getRelatedDataset() matches the field name on the originating table.
$dataset = MyTable::get($id);
$yform = $dataset->getForm();
$yform->setObjectparams('form_action', rex_getUrl(REX_ARTICLE_ID));
$yform->setActionField('showtext', ['', 'Saved']);
echo $dataset->executeForm($yform);
$dataset = MyTable::create();
$yform = $dataset->getForm();
$yform->setObjectparams('form_action', rex_getUrl(REX_ARTICLE_ID));
$yform->setActionField('showtext', ['', 'Saved']);
echo $dataset->executeForm($yform);
rex_yform_manager_collection (the result of find()) has these methods:
delete, filter, first, last, getIds, getValues, groupBy, isEmpty, map, save, setValue, shuffle, slice, sort, split, toKeyIndex, toKeyValue, isValueUnique, getUniqueValue, populateRelation
When a dataset is used in both backend (admin) and frontend (public) contexts, hide internal fields from frontend forms:
class MyDataset extends rex_yform_manager_dataset {
public function getFields(array $filter = []) {
$fields = $this->getTable()->getFields($filter);
if (rex::isBackend()) return $fields;
foreach ($fields as $i => $field) {
if (in_array($field->getName(), ['internal_links', 'admin_notes'])) {
unset($fields[$i]);
}
}
return $fields;
}
}
<?php
$members = team_member::query()
->where('status', 1)
->orderBy('prio')
->find();
?>
<ul class="team">
<?php foreach ($members as $m): ?>
<li>
<h3><?= rex_escape($m->getName()) ?></h3>
<?php if ($photo = $m->getPhotoMedia()): ?>
<img src="<?= rex_url::media($photo->getFileName()) ?>"
alt="<?= rex_escape($photo->getTitle()) ?>"
width="<?= $photo->getWidth() ?>"
height="<?= $photo->getHeight() ?>">
<?php endif; ?>
<?php if ($role = $m->getRole()): ?>
<p class="role"><?= rex_escape($role->getName()) ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
Hide a column from the Table Manager list view:
rex_extension::register('YFORM_DATA_LIST', function (rex_extension_point $ep) {
$list = $ep->getSubject();
$list->removeColumn('internal_field');
});
Custom column rendering:
rex_extension::register('YFORM_DATA_LIST', function (rex_extension_point $ep) {
$list = $ep->getSubject();
$list->setColumnFormat('image', 'custom', function ($params) {
$value = $params['list']->getValue('image');
if ($value) {
return '<img src="/media/' . rex_escape($value) . '" style="max-width:80px;">';
}
return '';
});
});
When you build custom links into the Table Manager backend (e.g. an "Edit" button from a frontend page that opens the backend), add the CSRF token:
$_csrf_key = $table->getCSRFKey();
$_csrf_params = rex_csrf_token::factory($_csrf_key)->getUrlParams();
// Append $_csrf_params to your URL
// Method 1: dump the SQL + params for a query
$query = MyTable::query()->where('status', 1);
rex_sql::factory()->setDebug()->getArray($query->getQuery(), $query->getParams());
// Method 2: global debug flag in dataset.php (set $debug = true)
| Class | Purpose |
|---|---|
rex_yform_manager_table | Table metadata & form generation |
rex_yform_manager_dataset | ORM entity (CRUD) |
rex_yform_manager_query | Query builder |
rex_yform_manager_collection | Dataset collection |
setModelClass() registered → returns plain rex_yform_manager_dataset instead of your subclass; method calls fail.getValue('photo') and treating the result as a rex_media instance – it's a filename string. Look up rex_media::get() separately.save() returns false on validation failure. Always check the return or log getMessages().find() then iterating – use findIds() and process in batches, or write a raw rex_sql query.whereListContains on a column that isn't a comma-separated list – matches accidentally on substrings.paginate($pager) without setting up rex_pager first with the page size – returns the default 20, may not match expectations.->alias('t') when using joinRelation – causes "ambiguous column" errors when the joined table has same-named columns.