A number of helper utilities designed to ease add-on development
During use, Add the requires section to addon.json to document the dependency
{
"require": {
"SV/StandardLib": [2001210000,"Standard Library by Xon v1.21.0+"],
"XF": ["2.2.0", "XenForo 2.2.0+"],
"php": ["7.2.0", "PHP 7.2.0+"]
}
}Note; SV/StandardLib should use a version_id and not a version_string to support sites which do not have the add-on installed yet.
Instead of matching on version_id, the addon.json's require section can match on addon version strings.
Uses version_compare under the hood after some very [i]basic[/i] standardization.
php version strings support dotted versions, '1.2.3' and also each part may also support special character strings:
any string not found in this list < dev < alpha = a < beta = b < RC | Release Candidate = rc < # < patch level | pl = p.
RLIKE operator support.
MySQL implements 'early row lookup' which results in the large select statement pulling in more data than is required. This trait allows migrating this with some minor configuration.
See Optimized List Queries add-on for examples.
Inject an entity relations at query time. This is useful to work-around XenForo lacking reverse relationships on handler-like entities
See https://github.com/Xon/XenForo2-OptimizedListQueries/blob/f3f0dbbcd58273314e92aca8a4b34d3ecc062815/upload/src/addons/SV/OptimizedListQueries/XF/Repository/Node.php#L12-L68 for an example. Be aware that MySQL can suffer degraded performance for large number of joins, so this might not always be the best choice.
Inject arbitrary SQL at query time, as join vs subquery can have massive performance differences despite being logically identical
Allows viewing template modifications which are applying to a template, including generated php source code
Called when a XF\Mvc\Router object is constructed to manipulate routes, as XF doesn't support chaining build_callbacks.
Usage example (using a Pub event hint):
public static function publicLinkBuilder(\SV\StandardLib\Repository\LinkBuilder $linkBuilder, \XF\Mvc\Router $router): void
{
$callable = function (string &$prefix, array &$route, string &$action, &$data, array &$params, \XF\Mvc\Router $router, bool &$suppressDefaultCallback) {
if (isset($data['foo']) {
return 'https://example.org';
} elseif (isset($data['bar']) {
return new RouteBuiltLink('https://example.org');
} elseif (isset($data['foobar']) {
// stop default build_callback usage, and use default XF processing
$suppressDefaultCallback = true;
}
return null; // default XF processing
}
$linkBuilder->injectLinkBuilderCallback($router, 'search', $callable);
}This is a helper repository designed to allow caching (and fetching) various permission in a way which can be extended
Helps get the user that owns an entity
$helperRepo = \SV\StandardLib\Helper::repo();
$user = $helperRepo->getUserEntity($entity);
if (!$user && ($entity->isValidGetter('Content') || $entity->isValidRelation('Content')))
{
$user = $helperRepo->getUserEntity($entity->get('Content'));
}
// XFRM support, as it doesn't have a User/Content relationship...
if (!$user && ($entity->isValidGetter('Resource') || $entity->isValidRelation('Resource')))
{
$user = $helperRepo->getUserEntity($entity->get('Resource'));
}Allows a single XenForo class extension to map to different concrete classes to support breaking changes in class structures. These aliases are XFCP compliant
<?php
namespace SV\ElasticSearchEssentials\XF\Repository;
\SV\StandardLib\Helper::repo()->aliasClass(
'SV\ElasticSearchEssentials\XF\Repository\Search',
\XF::$versionId < 2020000
? 'SV\ElasticSearchEssentials\XF\Repository\XF2\Search'
: 'SV\ElasticSearchEssentials\XF\Repository\XF22\Search'
);These methods accept ::class references, and have the return type hinted to match the argument.
$obj = Helper::repository(\XF\Repository\User::class);For static analysis and IDE, $obj will have the type \XF\Repository\User
Enrich a select box with choices.js.
Single select example:
<xf:macro name="svStandardLib_macros::choices_setup" />
<xf:selectrow name="select_row_example"
label="{{ phrase('example') }}"
data-xf-init="sv-choices"
data-placeholder="{{ phrase('example')|for_attr }}"
value="2">
<xf:option value="1">Option 1</xf:option>
<xf:option value="2">Option 2</xf:option>
<xf:option value="3">Option 3</xf:option>
</xf:selectrow>Multi-select example:
<xf:macro name="svStandardLib_macros::choices_setup" />
<xf:selectrow name="select_row_example"
label="{{ phrase('example') }}"
data-xf-init="sv-choices"
data-placeholder="{{ phrase('example')|for_attr }}"
data-max-item-count="2"
multiple="multiple"
value="{{ [1,2] }}">
<xf:option value="1">Option 1</xf:option>
<xf:option value="2">Option 2</xf:option>
<xf:option value="3">Option 3</xf:option>
</xf:selectrow>The initial structure is pre-rendered to reduce/prevent page jank.
To opt-out add the skip-rendering="true" attribute to the <xf:select> or <xf:selectrow> element
Load pagination pages via ajax instead of requiring full page-loads. Useful for overlays.
<xf:js src="sv/vendor/domurl/url.js" addon="SV/StandardLib" min="1" />
<xf:js src="sv/lib/ajaxPagination.js" addon="SV/Threadmarks" min="1" />
...
<div class="block" data-xf-init="sv-ajax-pagination" data-content-wrapper=".block-body--wrapper">
...
<div class="block-body--wrapper">
...
<xf:pagenav ... />
<xf:hiddenval name="final_url" value="{$finalUrl}" />
</div>
</div><xf:pagenav> and <xf:hiddenval name="final_url" /> must be inside the div which is tagged with data-content-wrapper's css selector
While similar to is_goggled, is_toggle_set supports specifying the default toggle state.
Stronlgy recommended to use toggle-storage-ex from sv/lib/storage.js.
Example of a default collapsed node-list:
<xf:js src="sv/lib/storage.js" addon="SV/StandardLib" min="1" />
<xf:set var="$isActive" value="{{ is_toggle_set($forum.node_id, false, 'node-toggle') ? ' is-active' : '' }}"/>
<div class="block block--collapsible-child-nodes">
<div class="block-container">
<h3 class="block-minorHeader collapseTrigger collapseTrigger--block {$isActive} "
data-target=".block--collapsible-child-nodes .block-body"
data-xf-click="toggle"
data-xf-init="toggle-storage-ex"
data-storage-type="cookie"
data-storage-container="node-toggle"
data-storage-key="{$forum.node_id}"
data-default-value="0"
>{{ phrase('sub_forums') }}</h3>
<div class="block-body toggleTarget {$isActive}">
...
</divAppend a value $newElementValue in an array/collection. If the array is null, then a new array is returned.
If $array is a collection, then the return value is a collection.
$array|addvalue($newElementValue)
Replaces a value $elementValue in an array/collection with $newElementValue. If $newElementValue is null, then that element is removed.
$array|replacevalue($elementValue, $newElementValue)
Reverse an array/collection. See array_reverse for details.
Computes the difference of arrays/collections. See array_diff for details.
Provide a dynamic count up/down timestamp
<abbr title="{$title|for_attr}" class="bbc-abbr">{{ sv_relative_timestamp($nowTimestamp, $otherTimestamp, $maximumDateParts, $countUp, 'bbc-time-counter') }}</abbr>Absolute value
Similar to parse_less_color, except this allows parsing an arbitrary LESS expression.
Enable the svLogLessFunc option to log debug information
Does not return CSS variables like parse_less_color does.
Backport phrase_dynamic from XF2.2 to XF2.1
Extend XF.Tabs to store the ID of the selected tab on submit via a hidden field.
Field name is set via data-sv-store-selected-tab-input-name added to the data-xf-init="tabs" element
<xf:js addon="SV/StandardLib" src="sv/lib/xf/core/structure.js" min="1" />
...
<div class="hScroller"
data-xf-init="h-scroller tabs"
data-panes=".js-categoryTypeTabPanes"
data-sv-store-selected-tab-input-name="category_type">Support for date/time/timezone input, returned as a unix timestamp
<xf:macro name="svStandardLib_helper_macros::date_time_input{{ $asRow ? '_row' : '' }}"
arg-name="scheduled_start_date"
arg-useNativeTimeInputs="{{ true }}"
arg-timestamp="{$xf.time}" />$scheduledStartDate = $this->filter('scheduled_start_date', 'sv-datetime');Compatibility note; $useNativeTimeInputs is forced enabled for XF2.3+.
For php code, sv-datetime input abstracts the differences between native time inputs and older inputs.
However, for javascript code referencing the individual fields this is a backwards compatibility breaking change.
InstallerHelper injects various setup helper features designed to allow robust installers.
Adds support for "require-soft" in addon.json which enables soft-dependencies
eg:
class Setup extends AbstractSetup
{
use SV\StandardLib\InstallerHelper;
use StepRunnerInstallTrait;
use StepRunnerUpgradeTrait;
use StepRunnerUninstallTrait;
public function installStep1(): void
{
$sm = $this->schemaManager();
foreach ($this->getTables() as $tableName => $callback)
{
$sm->createTable($tableName, $callback);
$sm->alterTable($tableName, $callback);
}
}
public function installStep2(): void
{
$sm = $this->schemaManager();
foreach ($this->getAlterTables() as $tableName => $callback)
{
if ($sm->tableExists($tableName))
{
$sm->alterTable($tableName, $callback);
}
}
}
public function upgrade2000000Step1(): void
{
$this->installStep1();
}
public function upgrade2000000Step2(): void
{
$this->installStep2();
}
public function uninstallStep1(): void
{
$sm = $this->schemaManager();
foreach ($this->getTables() as $tableName => $callback)
{
$sm->dropTable($tableName);
}
}
public function uninstallStep2(): void
{
$sm = $this->schemaManager();
foreach ($this->getRemoveAlterTables() as $tableName => $callback)
{
if ($sm->tableExists($tableName))
{
$sm->alterTable($tableName, $callback);
}
}
}
protected function getTables(): array
{
return [
'xf_sv_mytable' => function ($table) {
/** @var Create|Alter $table */
$this->addOrChangeColumn($table, 'id', 'int')->primaryKey();
},
];
}
protected function getAlterTables(): array
{
return [
'xf_user' => function (Alter $table) {
$this->addOrChangeColumn($table, 'sv_my_column', 'int')->setDefault(0);
},
];
}
protected function getRemoveAlterTables(): array
{
return [
'xf_user' => function (Alter $table) {
$table->dropColumns(['sv_my_column']);
},
];
}For simply table alters (add column/index, with column renames), the following can be used instead of defining getRemoveAlterTables;
public function uninstallStep2(): void
{
$sm = $this->schemaManager();
foreach ($this->getReversedAlterTables($this->getAlterTables()) as $tableName => $callback)
{
if ($sm->tableExists($tableName))
{
$sm->alterTable($tableName, $callback);
}
}
}This does not reverse column schema changes, which can be very complex to reverse.
BypassAccessStatus provides wrappers to easily get/set protected/private class values;
$accesser = new \SV\StandardLib\BypassAccessStatus();
$setUniqueEntityId = $accesser->setPrivate($this, '_uniqueEntityId', \XF\Mvc\Entity\Entity::class);
$getEntityCounter = $accesser->getStaticPrivate(\XF\Mvc\Entity\Entity::class, '_entityCounter');
$setEntityCounter = $accesser->setStaticPrivate(\XF\Mvc\Entity\Entity::class, '_entityCounter');
$id = $getEntityCounter();
$setEntityCounter($id + 1);