Convention-over-configuration utility and service helpers for XOOPS CMS development.
41 source files. 151 tests. Zero configuration. XSS-safe HTML by default. One composer require.
XOOPS Helpers is a standalone utility library that makes every XOOPS module safer and shorter by replacing the repetitive, error-prone boilerplate every module developer writes over and over:
// Before — scattered across every XOOPS module
$escaped = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$url = XOOPS_URL . '/modules/' . $dirname . '/article.php?id=' . $id;
$path = XOOPS_ROOT_PATH . '/modules/' . $dirname . '/language/' . $language . '/blocks.php';
$sitename = $GLOBALS['xoopsConfig']['sitename'];
// After
$escaped = HtmlBuilder::escape($value);
$url = Url::module($dirname, 'article.php', ['id' => $id]);
$path = Path::module($dirname, "language/{$language}/blocks.php");
$sitename = Config::get('system.sitename');That first line is not just shorter — it is structurally safer. Every manual
htmlspecialchars()call is a place where a future developer can introduce a stored XSS vulnerability by forgetting it once.HtmlBuilderescapes all attribute values and class names automatically — the source of the vast majority of real-world XSS. Tag content is your responsibility, intentionally: content can legitimately contain HTML (rendered markup, trusted template fragments). The safe path is explicit: pass user-supplied content throughHtmlBuilder::escape(). Thishtmlspecialcharspattern appears 30+ times in the XOOPS Core alone — each one a place where this library makes the correct choice the easiest choice.
- PHP 8.2 or later with the
ext-mbstringextension - No other runtime dependencies
Optional extensions for enhanced functionality:
ext-intl— locale-aware number and date formattingext-apcu— APCu caching backendext-zip— zip/unzip filesystem operations
composer require xoops/helpersuse Xoops\Helpers\Utility\HtmlBuilder;
use Xoops\Helpers\Service\Url;
use Xoops\Helpers\Service\Path;
use Xoops\Helpers\Service\Config;
use Xoops\Helpers\Service\Cache;
use Xoops\Helpers\Utility\Arr;
use Xoops\Helpers\Utility\Str;
use Xoops\Helpers\Utility\Number;
use Xoops\Helpers\Utility\Collection;
// HTML — attribute values escaped automatically; use text() for tag content
HtmlBuilder::attributes(['class' => 'btn', 'disabled' => true, 'data-id' => $userInput]);
HtmlBuilder::classes(['btn', 'btn-primary' => $isPrimary, 'disabled' => false]);
HtmlBuilder::tag('div', ['class' => 'alert'], HtmlBuilder::text($userMessage)); // user text
HtmlBuilder::tag('div', ['class' => 'body'], $renderedHtmlBlock); // trusted HTML
// URLs — zero concatenation
Url::module('news', 'article.php', ['id' => 42]);
Url::asset('themes/starter/css/style.css');
Url::theme('starter', 'images/logo.png');
// Paths — cross-platform, always correct; languageFile() resolves language fallback
Path::module('news', 'language/english/main.php');
Path::languageFile('news', $language, 'main.php'); // tries $language, falls back to english
Path::storage('caches/xmf');
Path::uploads('images/avatars');
// Config — dot notation, auto-cached
Config::get('system.sitename', 'XOOPS');
Config::get('news.items_per_page', 10);
// Cache — compute-and-cache in one call
$articles = Cache::remember('news_latest', 3600, fn() => loadArticles());
// Arrays — dot notation, pluck, group, filter
$value = Arr::get($config, 'database.host', 'localhost');
$names = Arr::pluck($users, 'uname', 'uid');
$grouped = Arr::groupBy($articles, 'category_id');
// Strings — slug, validation, case conversion
Str::slug('Hello World'); // "hello-world"
Str::isEmail('a@example.com'); // true
Str::camel('module_config'); // "moduleConfig"
Str::limit($body, 150); // "First 150 chars..."
Str::random(32); // cryptographically secure
// Numbers — human-readable formatting
Number::fileSize(1572864); // "1.50 MB"
Number::forHumans(2300000); // "2.3M"
Number::ordinal(21); // "21st"
Number::currency(99.99, 'EUR', 'de_DE');
// Collections — fluent data transformation
Collection::make($items)
->filter(fn($item) => $item['active'])
->sortBy('name')
->pluck('title', 'id')
->all();These work anywhere — CLI scripts, cron jobs, unit tests — no XOOPS boot required.
| Class | Purpose |
|---|---|
HtmlBuilder |
XSS-safe HTML: attributes, classes, tag, escape, text, stylesheet, script, meta — attribute values escaped automatically; use text() to explicitly escape tag content |
Arr |
Array helpers with dot notation: get, set, has, pluck, groupBy, sortBy, where, flatten, dot/undot, only/except, first/last, wrap, collapse |
Str |
String helpers: slug, camel/snake/studly/kebab, limit, random, contains/startsWith/endsWith, between, mask, isEmail/isUrl/isIp/isJson/isHexColor |
Number |
Number formatting: format, fileSize, forHumans, percentage, ordinal, currency, clamp |
Date |
Date helpers with injectable time source: now, range, diff, isValid, addDays/subDays, isWeekend/isToday/isPast/isFuture, reformat, age |
Value |
Value resolution: value (Closure resolver), blank/filled, optional (null-safe access), once (memoization), missing (sentinel) |
Collection |
Fluent array wrapper: map, filter, reject, reduce, pluck, groupBy, sortBy, first/last, chunk, take/skip, sum/avg/min/max, when, pipe, tap |
Pipeline |
Data transformation chains: Pipeline::send($v)->pipe(fn)->pipe(fn)->thenReturn() |
Stringable |
Fluent string builder: Stringable::of($s)->trim()->lower()->slug()->toString() |
Filesystem |
File operations: readJson/putJson, mimeType, isImage, mkdir, deleteDirectory, copyDirectory, zip/unzip, readChunked |
Environment |
Runtime detection: isProduction/isDevelopment/isTesting, get/require/has |
Benchmark |
Profiling: measure (time + memory), time, average (multi-iteration) |
Encoding |
URL-safe base64: base64UrlEncode/base64UrlDecode |
Data |
Conversion: toArray, toObject, toQueryString, fromQueryString |
Retry |
Error recovery: retry (with backoff), rescue (with fallback) |
ThrowHelper |
Guard clauses: throwIf, throwUnless |
Transform |
Conditional transforms: transform (if filled), when (predicate-based) |
Tap |
Side-effect helper: call callback, return original value |
| Interface | Purpose |
|---|---|
PathLocatorInterface |
Filesystem path resolution |
UrlGeneratorInterface |
URL generation |
CacheInterface |
Cache operations |
ConfigProviderInterface |
Configuration loading |
DateTimeProviderInterface |
Clock abstraction for testing |
| Facade | Purpose | Override |
|---|---|---|
Path |
Path::base(), module(), storage(), uploads(), themes(), languageFile() |
Path::use($locator) |
Url |
Url::to(), asset(), module(), theme() |
Url::use($generator) |
Config |
Config::get(), set(), has(), all(), registerLoader() |
Config::setProvider($p) |
Cache |
Cache::get(), set(), forget(), remember(), flush() |
Cache::use($adapter) |
All facades work immediately using XOOPS constants (XOOPS_ROOT_PATH, XOOPS_URL, etc.). Override with ::use() for testing or custom installations. Reset with ::reset().
| Provider | Purpose |
|---|---|
DefaultPathLocator |
Maps to XOOPS constants |
DefaultUrlGenerator |
Uses XOOPS_URL, falls back to $_SERVER |
XoopsCacheAdapter |
Auto-detects: XoopsCache, APCu, or file cache |
ArrayCache |
In-memory cache for testing |
SystemDateTimeProvider |
System clock |
| Component | Purpose |
|---|---|
XoopsCollection |
XoopsCollection::fromHandler($handler, $criteria) with pluckVar() for getVar() |
AssetUrlPlugin |
Smarty: <{asset_url path="css/style.css"}> |
FormatNumberPlugin |
Smarty: <{format_number value=$size type="filesize"}> |
CssClassesPlugin |
Smarty: <{css_classes classes=$classArray}> |
PluginRegistrar |
Register all Smarty plugins at once |
| Component | Purpose |
|---|---|
Tappable |
Trait adding tap() to any class |
functions.php |
Optional global function wrappers (not auto-loaded) |
Dependencies flow downward only. Tier 0 classes can be used in any PHP 8.2+ project without XOOPS — in CLI scripts, cron jobs, and unit tests with no bootstrap required.
graph TD
T4["**Tier 4 · Integration**
Depend on XOOPS classes
XoopsObject · Smarty"]
T3["**Tier 3 · Provider**
Default implementations
XOOPS-aware"]
T2["**Tier 2 · Service**
Static facades
Depend on XOOPS constants"]
T1["**Tier 1 · Contracts**
Interfaces only
No implementation"]
T0["**Tier 0 · Utility**
Pure PHP · Zero dependencies
Works anywhere"]
T4 --> T3
T3 --> T2
T2 --> T1
T1 --> T0
style T4 fill:#b7e0ff,stroke:#4a90d9,color:#000
style T3 fill:#c8f0d0,stroke:#3a9a5c,color:#000
style T2 fill:#fff3b0,stroke:#c8a200,color:#000
style T1 fill:#ffd6a5,stroke:#d48000,color:#000
style T0 fill:#ffadad,stroke:#c0392b,color:#000
The file src/functions.php provides short function wrappers like collect(), str(), pipeline(), tap(), retry(), env(), etc. It is not auto-loaded — opt in explicitly.
Recommended pattern: load it once in your XOOPS bootstrap, not in individual module files. This prevents redundant require calls across a multi-module installation:
// In mainfile.php or a central preload script — once per request
if (file_exists(XOOPS_ROOT_PATH . '/vendor/xoops/helpers/src/functions.php')) {
require_once XOOPS_ROOT_PATH . '/vendor/xoops/helpers/src/functions.php';
}If you are building a single module and do not control the bootstrap, load it in your module's entry point:
require_once 'vendor/xoops/helpers/src/functions.php';
$slug = str('Hello World')->slug()->toString();
$data = collect($items)->filter(fn($i) => $i['active'])->pluck('name')->all();
$value = retry(3, fn() => fetchFromApi(), sleepMs: 500);All functions are guarded with function_exists() to prevent fatal redeclaration errors.
Fully compatible. Designed for inclusion in XOOPS 2.5.12+.
No conflicts. Different namespace (Xoops\Helpers\ vs Xmf\), no shared class names, no shared global functions. Both can be loaded simultaneously via Composer.
Where both libraries offer related functionality, they serve different scopes:
| Area | XMF 1.x | XOOPS Helpers |
|---|---|---|
| URL/Path | $helper->url() — module-scoped |
Url::module() — global, works without module context |
| Config | $helper->getConfig() — per-module handler |
Config::get('mod.key') — dot notation, cached |
| Cache | Helper\Cache::cacheRead() — module-prefixed |
Cache::remember() — global, auto-backend |
| Random | Random::generateKey() — SHA512 hash tokens |
Str::random() — URL-safe strings, configurable length |
| SEO | Metagen::generateSeoTitle() — full meta tags |
Str::slug() — pure string transformation |
You do not need to refactor existing XMF 1.x code to adopt this library. Both coexist safely. The recommended approach depends on where you are in a project:
Starting a new module — use XOOPS Helpers exclusively from the first line. There is no legacy to consider and you get the full benefit of automatic escaping, dot-notation config, and fluent collections from day one.
Actively developing an existing module — use XOOPS Helpers for all new code and any functions you touch during the current sprint. When you open a file to add a feature, convert the XMF 1.x patterns in that file as you go. Do not schedule a dedicated refactoring sprint; let the migration happen organically as the module evolves.
Maintaining a stable module with no active development — do nothing. The libraries coexist with zero conflicts. The migration cost is not justified by a pure maintenance ticket. If it is not broken, leave it until you have a reason to open the file.
When you do migrate a specific pattern, the Cache facade is the most common conversion:
// XMF 1.x — before
if (!$data = \XoopsCache::read("{$dirname}_config")) {
$data = xoops_getModuleConfig($dirname);
\XoopsCache::write("{$dirname}_config", $data);
}
// XOOPS Helpers — after
$data = Cache::remember("{$dirname}_config", 3600, fn() => xoops_getModuleConfig($dirname));Designed as a companion. XMF 2.0 provides the architectural framework (Repository, EventBus, Container, QueryBuilder); XOOPS Helpers provides the day-to-day utilities (Arr, Str, Number, HtmlBuilder, Collection). XMF 2.0 will declare xoops/helpers as a dependency — requiring XMF 2.0 pulls this library in automatically.
composer install
vendor/bin/phpunitAll services are mockable for testing:
use Xoops\Helpers\Service\{Path, Url, Config, Cache};
use Xoops\Helpers\Provider\ArrayCache;
// Inject test implementations
Cache::use(new ArrayCache());
Config::registerLoader('mymod', fn() => ['key' => 'value']);
// Reset after tests
Cache::reset();
Config::reset();
Path::reset();
Url::reset();The Date utility accepts an injectable time provider:
use Xoops\Helpers\Utility\Date;
use Xoops\Helpers\Contracts\DateTimeProviderInterface;
Date::setProvider(new class implements DateTimeProviderInterface {
public function now(): \DateTimeImmutable {
return new \DateTimeImmutable('2025-06-15 12:00:00');
}
});
Date::isToday('2025-06-15'); // true — deterministic in tests
Date::resetProvider();Contributions are welcome. Please follow XOOPS coding standards:
declare(strict_types=1)in every file- PHP 8.2+ features (readonly, match, named arguments, union types)
- Final classes for utility classes
- Full type hints on all methods
- PHPUnit tests for all new functionality
See TUTORIAL.md for a comprehensive guide with before/after comparisons from real XOOPS Core and module code.
GNU GPL v2 or later. See LICENSE for details.