ViewModels

Cubex separates data (Models/ViewModels) from presentation (Views). This pattern keeps business logic out of templates and makes models independently testable and JSON-serializable.

Class Hierarchy

classDiagram
    class Model {
        <<interface>>
        +jsonSerialize() mixed
    }
    class View {
        <<interface>>
        +setModel(Model data)
        +render() string
    }
    class ViewModel {
        +setView(string viewClass) self
        +createView() View
        +lock() self
    }
    class AbstractView {
        <<abstract>>
        +setModel(Model data)
        +render() string
    }
    class TemplatedViewModel {
        +render() string
        +addVariant(string variant) self
    }
    class JsonView {
        +setFlags(int flags) self
        +render() string
    }
    class ArrayModel {
        +set(array data) self
        +addItem(value, key) self
    }

    Model <|.. ViewModel
    View <|.. AbstractView
    ViewModel <|-- TemplatedViewModel
    View <|.. TemplatedViewModel
    AbstractView <|-- JsonView
    ViewModel <|-- ArrayModel

ViewModel

The primary data container. Holds data as public properties, supports JSON serialization, and can create a corresponding View:

use Cubex\ViewModel\ViewModel;

class UserProfileModel extends ViewModel
{
  protected string $_defaultView = UserProfileView::class;

  public string $name;
  public string $email;
  public int $age;
}

Creating and Using

$model = new UserProfileModel();
$model->name = 'Alice';
$model->email = 'alice@example.com';
$model->age = 30;

// Create the associated view and render
$view = $model->createView();
$html = $view->render();

// Or serialize to JSON
$json = json_encode($model);
// {"name":"Alice","email":"alice@example.com","age":30}

Locking (Immutability)

Call lock() to freeze the model’s state. After locking, property values are captured and the model becomes read-only:

$model->lock();

// Properties are still readable
echo $model->name; // "Alice"

// But setting throws an exception
$model->name = 'Bob'; // Throws: "Cannot set property 'name' ... is immutable"

Locked models serialize from their frozen snapshot rather than live properties.

Custom View Override

Override the view class at creation time:

$view = $model->createView(MobileUserProfileView::class);

View

Views receive a model and render it to a string. Implement the View interface:

use Cubex\ViewModel\View;
use Cubex\ViewModel\Model;

class UserProfileView implements View
{
  private Model $_model;

  public function setModel(Model $data): void
  {
    $this->_model = $data;
  }

  public function render(): string
  {
    return "<div class='profile'>"
      . "<h1>{$this->_model->name}</h1>"
      . "<p>{$this->_model->email}</p>"
      . "</div>";
  }
}

AbstractView

A convenient base class that stores the model and provides a _render() hook:

use Cubex\ViewModel\AbstractView;

class UserCardView extends AbstractView
{
  protected function _render(): ?ISafeHtmlProducer
  {
    // Access the model via $this->_model
    return new SafeHtml("<div>{$this->_model->name}</div>");
  }
}

TemplatedViewModel

Combines ViewModel and View into a single class. Renders using .phtml template files located alongside the class file:

use Cubex\ViewModel\TemplatedViewModel;

class DashboardPage extends TemplatedViewModel
{
  public int $userCount;
  public int $orderCount;
  public array $recentOrders;
}

With a template at DashboardPage.phtml in the same directory:

<!-- DashboardPage.phtml -->
<div class="dashboard">
  <h1>Dashboard</h1>
  <p>Users: <?= $this->userCount ?></p>
  <p>Orders: <?= $this->orderCount ?></p>
  <ul>
    <?php foreach($this->recentOrders as $order): ?>
      <li><?= $order['id'] ?>: <?= $order['total'] ?></li>
    <?php endforeach; ?>
  </ul>
</div>

Template Variants

Add variant templates that take priority over the default. Useful for device-specific or A/B test rendering:

$page = new DashboardPage();
$page->addVariant('mobile');
// Looks for DashboardPage.mobile.phtml first,
// falls back to DashboardPage.phtml
echo $page->render();

Self-Rendering

TemplatedViewModel acts as its own view. Calling createView() without an override returns $this:

$page = new DashboardPage();
$view = $page->createView(); // Returns $page itself
echo $view->render();        // Renders the template

JsonView

Renders any Model as JSON:

use Cubex\ViewModel\JsonView;

$model = new UserProfileModel();
$model->name = 'Alice';
$model->email = 'alice@example.com';

$view = new JsonView();
$view->setModel($model);
$view->setFlags(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
echo $view->render();

ArrayModel

A ViewModel backed by a simple array instead of typed properties:

use Cubex\ViewModel\ArrayModel;

$model = new ArrayModel();
$model->addItem('Alice', 'name');
$model->addItem('alice@example.com', 'email');
$model->set(['tags' => ['admin', 'user']]);

echo json_encode($model);
// {"name":"Alice","email":"alice@example.com","tags":["admin","user"]}

ViewModel Flow in Controllers

When a controller method returns a ViewModel, the framework handles view creation and rendering automatically:

flowchart LR
    Controller["Controller returns<br/>ViewModel"] --> SetCtx["Set context/cubex"]
    SetCtx --> CreateView["createView()"]
    CreateView --> SetModel["setModel()"]
    SetModel --> Render["render()"]
    Render --> Response["CubexResponse"]
class ProfileController extends Controller
{
  public function getIndex(): UserProfileModel
  {
    $model = new UserProfileModel();
    $model->name = 'Alice';
    $model->email = 'alice@example.com';
    // Controller::_prepareResponse() handles the rest
    return $model;
  }
}

This site uses Just the Docs, a documentation theme for Jekyll.