PHPackages                             timefrontiers/php-multiform - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. timefrontiers/php-multiform

ActiveLibrary[Database &amp; ORM](/categories/database)

timefrontiers/php-multiform
===========================

Dynamic table entity class with runtime table resolution

v1.0.1(1mo ago)0231MITPHPPHP &gt;=8.1

Since Apr 15Pushed 1mo agoCompare

[ Source](https://github.com/timefrontiers/php-multiform)[ Packagist](https://packagist.org/packages/timefrontiers/php-multiform)[ RSS](/packages/timefrontiers-php-multiform/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (2)Dependencies (3)Versions (3)Used By (1)

TimeFrontiers PHP Multiform
===========================

[](#timefrontiers-php-multiform)

Dynamic table entity class with runtime table resolution.

[![PHP Version](https://camo.githubusercontent.com/04744bae0a61d2ffe29c26f07a9612eae20445fc6feaeb77b3af1f0e9be6447c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d3838393242462e737667)](https://php.net/)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

Installation
------------

[](#installation)

```
composer require timefrontiers/php-multiform
```

Requirements
------------

[](#requirements)

- PHP 8.1+
- `timefrontiers/php-database-object` ^1.0
- `timefrontiers/php-pagination` ^1.0

When to Use
-----------

[](#when-to-use)

Unlike the `DatabaseObject` trait which requires static table definitions, Multiform allows runtime table resolution. Use it for:

- Multi-tenant databases with per-tenant tables
- Sharded tables (users\_1, users\_2, etc.)
- Generic CRUD operations on arbitrary tables
- Admin panels and data browsers
- Dynamic form builders

Quick Start
-----------

[](#quick-start)

```
use TimeFrontiers\Database\Multiform;

// Create
$form = new Multiform('mydb', 'users');
$form->name = 'John Doe';
$form->email = 'john@example.com';
$form->save();

// Or use factory method
$form = Multiform::from('mydb', 'users')
  ->fill(['name' => 'Jane', 'email' => 'jane@example.com']);
$form->save();

// Find by ID
$user = Multiform::from('mydb', 'users')->findById(123);

// Query builder
$users = Multiform::from('mydb', 'users')
  ->query()
  ->where('status', 'active')
  ->orderBy('name')
  ->get();
```

Connection Management
---------------------

[](#connection-management)

```
// Instance-level
$form = new Multiform('mydb', 'users', 'id', $conn);
// Or
$form->setConnection($conn);

// Class-level (all instances)
Multiform::useConnection($conn);

// Global fallback
global $database;
$database = new SQLDatabase(...);
```

Property Access
---------------

[](#property-access)

Multiform uses magic methods for dynamic property access:

```
$form = new Multiform('mydb', 'products');

// Set properties
$form->name = 'Widget';
$form->price = 29.99;
$form->status = 'active';

// Get properties
echo $form->name;     // "Widget"
echo $form->price;    // 29.99
echo $form->getId();  // Primary key value

// Fill from array
$form->fill([
  'name' => 'Gadget',
  'price' => 49.99,
]);

// Convert to array
$data = $form->toArray();
```

Query Builder
-------------

[](#query-builder)

### Basic Queries

[](#basic-queries)

```
$table = Multiform::from('mydb', 'users');

// Find all
$users = $table->findAll();

// Find by ID
$user = $table->findById(123);

// Count
$count = $table->countAll();

// Check existence
$exists = $table->valueExists('email', 'john@example.com');
```

### Fluent Queries

[](#fluent-queries)

```
$table = Multiform::from('mydb', 'orders');

// WHERE conditions
$orders = $table->query()
  ->where('status', 'pending')
  ->where('total', '>', 100)
  ->get();

// OR conditions
$orders = $table->query()
  ->where('status', 'shipped')
  ->orWhere('priority', 'high')
  ->get();

// IN / NOT IN
$orders = $table->query()
  ->whereIn('status', ['pending', 'processing'])
  ->whereNotIn('customer_id', [1, 2, 3])
  ->get();

// NULL checks
$orders = $table->query()
  ->whereNull('shipped_at')
  ->whereNotNull('paid_at')
  ->get();

// Ordering & Pagination
$orders = $table->query()
  ->orderBy('created_at', 'DESC')
  ->limit(10)
  ->offset(20)
  ->get();

// First result
$order = $table->query()
  ->where('status', 'pending')
  ->first();

// Count & Exists
$count = $table->query()->where('status', 'pending')->count();
$exists = $table->query()->where('email', 'test@example.com')->exists();
```

### Custom SQL

[](#custom-sql)

```
$table = Multiform::from('mydb', 'products');

$results = $table->findBySql(
  "SELECT * FROM :database:.:table:
   WHERE category = ? AND price < ?
   ORDER BY :primary_key: DESC",
  ['electronics', 500]
);
```

CRUD Operations
---------------

[](#crud-operations)

### Create

[](#create)

```
$product = new Multiform('store', 'products');
$product->name = 'Widget';
$product->price = 29.99;
$product->stock = 100;

if ($product->save()) {
  echo "Created with ID: " . $product->getId();
} else {
  $errors = $product->getErrors();
}
```

### Update

[](#update)

```
$product = Multiform::from('store', 'products')->findById(123);
$product->price = 34.99;

if (!$product->save()) {
  $errors = $product->getErrors();
}
```

### Delete

[](#delete)

```
$product = Multiform::from('store', 'products')->findById(123);

if (!$product->delete()) {
  $errors = $product->getErrors();
}
```

Timestamps &amp; Author
-----------------------

[](#timestamps--author)

Multiform automatically handles these fields if they exist in the table:

FieldBehavior`_created`Set to current datetime on insert`_updated`Set to current datetime on insert/update`_author`Set from `$session->name` on insert```
$form = new Multiform('mydb', 'posts');
$form->title = 'Hello World';
$form->save();

echo $form->created();  // "2024-01-15 10:30:00"
echo $form->updated();  // "2024-01-15 10:30:00"
echo $form->author();   // "john_doe"
```

Dirty Tracking
--------------

[](#dirty-tracking)

Track changes to entity data:

```
$user = Multiform::from('mydb', 'users')->findById(123);

$user->name = 'New Name';

// Check if anything changed
if ($user->isDirty()) {
  // Get changed fields
  $changes = $user->getDirty();
  // ['name' => 'New Name']
}

// Check specific field
if ($user->isDirty('name')) {
  // name was changed
}
```

Multi-Tenant Example
--------------------

[](#multi-tenant-example)

```
class TenantDatabase {

  private string $tenant_id;

  public function __construct(string $tenant_id) {
    $this->tenant_id = $tenant_id;
  }

  public function table(string $table):Multiform {
    // Each tenant has their own database
    $database = "tenant_{$this->tenant_id}";
    return Multiform::from($database, $table);
  }
}

// Usage
$tenant = new TenantDatabase('acme_corp');

$users = $tenant->table('users')
  ->query()
  ->where('status', 'active')
  ->get();

$newUser = $tenant->table('users');
$newUser->name = 'John';
$newUser->save();
```

Sharded Table Example
---------------------

[](#sharded-table-example)

```
function getUserShard(int $user_id):Multiform {
  $shard = $user_id % 4;  // 4 shards
  return Multiform::from('users_db', "users_{$shard}");
}

// Find user across shards
$user = getUserShard(12345)->findById(12345);

// Query specific shard
$users = getUserShard(0)
  ->query()
  ->where('status', 'active')
  ->get();
```

Admin Panel Example
-------------------

[](#admin-panel-example)

```
// Generic data browser
function browseTable(string $database, string $table, array $filters = []):array {
  $query = Multiform::from($database, $table)->query();

  foreach ($filters as $field => $value) {
    $query->where($field, $value);
  }

  return $query->orderByDesc('_created')->take(50)->get();
}

// Generic record editor
function updateRecord(string $database, string $table, int $id, array $data):bool {
  $record = Multiform::from($database, $table)->findById($id);

  if (!$record) {
    return false;
  }

  return $record->fill($data)->save();
}
```

Pagination
----------

[](#pagination)

Both `Multiform` and `MultiformQuery` include the `Pagination` trait from `timefrontiers/php-pagination`.

### Via the query builder (recommended)

[](#via-the-query-builder-recommended)

`MultiformQuery::paginate()` counts matching rows, then fetches the current page in one call. It returns an array with `data` (hydrated `Multiform` instances) and `meta` (pagination metadata).

```
// Explicit page / per-page
$result = Multiform::from('mydb', 'orders')
  ->query()
  ->where('status', 'pending')
  ->orderBy('_created', 'DESC')
  ->paginate(page: 2, per_page: 25);

foreach ($result['data'] as $order) {
  echo $order->id;
}

// $result['meta'] shape:
// [
//   'current_page'  => 2,
//   'per_page'      => 25,
//   'total'         => 87,
//   'total_pages'   => 4,
//   'from'          => 26,
//   'to'            => 50,
//   'has_more'      => true,
//   'is_first_page' => false,
//   'is_last_page'  => false,
// ]
```

#### Auto-read from request

[](#auto-read-from-request)

When `$page` / `$per_page` are omitted, values are read from `$_GET` (then `$_POST`) using the supplied key names — same behaviour as `Pagination::fromRequest()`.

```
// Reads ?page=3&per_page=10 from the request automatically
$result = Multiform::from('mydb', 'products')->query()->paginate();

// Custom request keys (?p=3&limit=10)
$result = Multiform::from('mydb', 'products')
  ->query()
  ->paginate(page_key: 'p', per_page_key: 'limit');
```

#### API response

[](#api-response)

```
$result = Multiform::from('mydb', 'users')
  ->query()
  ->where('active', 1)
  ->paginate();

return json_encode([
  'data'       => array_map(fn($u) => $u->toArray(), $result['data']),
  'pagination' => $result['meta'],
]);
```

### Via the Multiform instance

[](#via-the-multiform-instance)

Because `Multiform` itself uses the `Pagination` trait you can also manage pagination state on the instance and pass `limitClause()` into a custom SQL query.

```
$table = Multiform::from('mydb', 'users');

// Configure from request or explicitly
$table->fromRequest();
// or: $table->setPage(2)->setPerPage(25);

// Get total count for the full result set
$table->setTotalCount($table->countAll());

// Query with LIMIT/OFFSET baked in
$users = $table->findBySql(
  "SELECT * FROM :database:.:table:
   WHERE active = 1
   ORDER BY name ASC
   {$table->limitClause()}",
);

echo "Page {$table->currentPage()} of {$table->totalPages()}";
echo "Showing {$table->itemStart()}–{$table->itemEnd()} of {$table->totalCount()}";

// Pagination links
foreach ($table->pageRange(2) as $page) {
  if ($page === null) {
    echo '…';
  } else {
    $active = $page === $table->currentPage() ? 'active' : '';
    echo "{$page}";
  }
}
```

### Pagination meta helpers

[](#pagination-meta-helpers)

All methods from the `Pagination` trait are available on both the `Multiform` instance and (via the query builder result) indirectly through the `meta` array. Key methods:

MethodReturnsDescription`setPage(int)``static`Set current page`setPerPage(int)``static`Set items per page (1–1000)`setTotalCount(int)``static`Set total item count`fromRequest(...)``static`Load page/per\_page from `$_GET``currentPage()``int`Current page`totalPages()``int`Total pages`offset()``int`SQL OFFSET value`limitClause()``string``"LIMIT X OFFSET Y"` string`hasPreviousPage()``bool`Previous page exists`hasNextPage()``bool`Next page exists`pageRange(int)``array`Page numbers with `null` for ellipsis`pageUrl(int, ...)``string`URL for a specific page`paginationToArray()``array`Full metadata array`paginationMeta(...)``array`Metadata + prev/next linksError Handling
--------------

[](#error-handling)

```
$form = new Multiform('mydb', 'products');
$form->name = 'Widget';

if (!$form->save()) {
  // Check for errors
  if ($form->hasErrors('_create')) {
    $message = $form->firstError('_create');
  }

  // Get all errors
  $errors = $form->getErrors();

  // Use with InstanceError for rank-based filtering
  $visibleErrors = (new InstanceError($form))->get('_create');
}
```

License
-------

[](#license)

MIT License

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

2

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/dfcd9a5645cb2deb0771b9102be193d63b495564b1e06c0b85cb31bf028d0887?d=identicon)[ikechukwuokalia](/maintainers/ikechukwuokalia)

---

Top Contributors

[![ikechukwuokalia](https://avatars.githubusercontent.com/u/4541227?v=4)](https://github.com/ikechukwuokalia "ikechukwuokalia (4 commits)")

---

Tags

phpdatabaseormdynamic tablemulti-table

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/timefrontiers-php-multiform/health.svg)

```
[![Health](https://phpackages.com/badges/timefrontiers-php-multiform/health.svg)](https://phpackages.com/packages/timefrontiers-php-multiform)
```

###  Alternatives

[modul-is/orm

Lightweight hybrid ORM/Explorer

1118.7k](/packages/modul-is-orm)[bauer01/unimapper

Universal mapping tool for collecting data from different sources

102.6k6](/packages/bauer01-unimapper)[riverside/php-orm

PHP ORM micro-library and query builder

121.3k](/packages/riverside-php-orm)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
