ValuesObject Pattern
A ValuesObject (VO) is a typed wrapper around one database row. Plugins should never pass raw associative arrays around — every result that leaves a DataAccessObject should first be converted to a VO.
Why
- Type safety. A VO has typed getters; an array does not.
- Refactor safety. Renaming a column updates one constant in one
place. Searching for
'changefreq'strings in plugin code is brittle. - Domain language. Calls become
getChangeFrequency()instead of['changefreq']. The domain term, not the storage detail. - Consistent enums. Status values, types, and other enums live as constants on the VO class so every caller agrees on the spelling.
Anatomy
namespace Plugins\Contents\Domain\Model;
use ValuesObject;
class SitemapUrlValuesObject extends ValuesObject
{
public const FIELD_URL = 'url';
public const FIELD_CHANGE_FREQUENCY = 'changefreq';
public const FIELD_PRIORITY = 'priority';
public const FIELD_LAST_MODIFIED = 'lastmod';
public const FIELD_STATUS = 'status';
public const STATUS_ACTIVE = 'active';
public const STATUS_DISABLED = 'disabled';
public function getUrl(): string
{
return $this->get(static::FIELD_URL);
}
public function getChangeFrequency(): string
{
return $this->get(static::FIELD_CHANGE_FREQUENCY);
}
public function getPriority(): string
{
return number_format((float) $this->get(static::FIELD_PRIORITY), 1);
}
public function getLastModified(): ?string
{
return $this->get(static::FIELD_LAST_MODIFIED) ?: null;
}
public function getStatus(): string
{
return $this->get(static::FIELD_STATUS);
}
}
Required Parts
FIELD_*constants for every column the plugin reads or writes.- Enum constants (
STATUS_*,TYPE_*, etc.) for every column with a fixed set of allowed values. - A getter for every field, named after the domain term, not the
column. Use the long form:
getChangeFrequency(), notgetChangefreq(). See Naming.md. - The class name is singular, even when the table is plural:
SitemapUrlValuesObjectfor tablesitemap_urls.
Using FIELD Constants in DAO Calls
Plugin code should never pass column-name strings to the DAO. Use the constant from the VO:
// Bad — magic strings
$search = [
'status' => 'active',
'url&IS NOT' => null,
];
$rows = $this->object->search($search);
// Good — constants
$search = [
SitemapUrlValuesObject::FIELD_STATUS => SitemapUrlValuesObject::STATUS_ACTIVE,
SitemapUrlValuesObject::FIELD_URL.'&IS NOT' => null,
];
$rows = $this->object->search($search);
When a column is renamed later, only the constant changes. The plugin code keeps working without a search-and-replace pass.
Converting DAO Results
DataAccessObject methods return arrays. Wrap them with convert() so
plugin code receives typed objects:
public function getSitemapUrls(array $search): array
{
$rows = $this->select($this->getSql(), $search);
return SitemapUrlValuesObject::convert($rows);
}
convert() is provided by the base ValuesObject class. It accepts an
array of rows and returns an array of VO instances. Empty input returns
an empty array.
After conversion, callers iterate over typed objects:
$rows = $this->object->getSitemapUrls($search);
foreach ($rows as $row) {
$urls[] = [
'loc' => $host.$row->getUrl(),
'changefreq' => $row->getChangeFrequency(),
'priority' => $row->getPriority(),
'lastmod' => $row->getLastModified(),
];
}
Common Mistakes Caught in Code Review
- Returning raw arrays from a DAO method. Always
convert()first. - Reading
'status'literally in a plugin instead ofFooValuesObject::FIELD_STATUS. - Comparing against
'active'instead ofFooValuesObject::STATUS_ACTIVE. - Plural class names (
SitemapUrlsValuesObject). - Missing getters — accessing
$row->get('field')directly from a plugin defeats the type safety the VO is supposed to provide. - Acronym getters (
getCdate(),getMdate()).