Files
2026-04-28 15:13:50 +02:00

787 lines
29 KiB
PHP

<?php
/**
* Global helper functions for the Complianz Terms & Conditions plugin.
*
* Provides utility functions used across the plugin for template rendering,
* option retrieval, UI notifications, HTML sanitisation, language handling,
* document condition callbacks, and capability checks. All functions are
* wrapped in function_exists() guards so themes or other plugins can override
* them when needed.
*
* @package Complianz_Terms_Conditions
* @author Complianz
* @copyright 2023 Complianz.io
* @license GPL-2.0-or-later
* @link https://complianz.io
*
* @since 1.0.0
*/
defined( 'ABSPATH' ) || die( 'you do not have acces to this page!' );
if ( ! function_exists( 'cmplz_tc_get_template' ) ) {
/**
* Loads and returns a plugin template file, with optional theme override support.
*
* Resolves the template path via the `cmplz_tc_template_file` filter, then checks
* whether the active theme provides an override in its own directory under the
* plugin's slug folder. PHP templates are executed with output buffering so any
* PHP logic inside them is evaluated; non-PHP files (e.g. HTML) are read directly.
* Named placeholders in the form `{key}` are replaced with values from `$args`.
*
* @since 1.0.0
*
* @param string $filename Template filename relative to the plugin's `templates/`
* directory (e.g. 'wizard/menu.php').
* @param array $args Associative array of placeholder replacements. Each key
* maps to a `{key}` token in the template.
* Example: array( 'title' => 'My Title', 'content' => '<p>...</p>' ).
* @return string|false Rendered template string, or false if the plugin-side
* template file does not exist.
*/
function cmplz_tc_get_template( $filename, $args = array() ) {
/**
* Filters the resolved path to a plugin template file.
*
* @since 1.0.0
*
* @param string $file Absolute path to the plugin template file.
* @param string $filename Template filename relative to the templates/ directory.
*/
$file = apply_filters( 'cmplz_tc_template_file', trailingslashit( cmplz_tc_path ) . 'templates/' . $filename, $filename );
// Build the path where the active theme could provide a template override.
$theme_file = trailingslashit( get_stylesheet_directory() )
. trailingslashit( basename( cmplz_tc_path ) )
. 'templates/' . $filename;
// Return false early when the plugin-bundled template is missing entirely.
if ( ! file_exists( $file ) ) {
return false;
}
// Use the theme override when it exists.
if ( file_exists( $theme_file ) ) {
$file = $theme_file;
}
if ( strpos( $file, '.php' ) !== false ) {
// Execute PHP templates with output buffering to capture dynamic output.
ob_start();
require $file;
$contents = ob_get_clean();
} else {
// Read static (non-PHP) template files directly.
$contents = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local file path, not a remote URL.
}
// Replace {key} placeholder tokens with the provided argument values.
if ( ! empty( $args ) ) {
foreach ( $args as $fieldname => $value ) {
$contents = str_replace( '{' . $fieldname . '}', $value, $contents );
}
}
return $contents;
}
}
if ( ! function_exists( 'cmplz_tc_settings_page' ) ) {
/**
* Returns the URL of the plugin's admin settings page.
*
* When the full Complianz suite is active (detected via the `cmplz_version`
* constant), the page is hosted under `admin.php`; on standalone installs it
* falls back to `tools.php`.
*
* @since 1.0.0
*
* @return string Absolute URL to the Terms & Conditions settings page.
*/
function cmplz_tc_settings_page() {
if ( ! defined( 'cmplz_version' ) ) {
// Standalone install: page lives under Tools.
return add_query_arg( array( 'page' => 'terms-conditions' ), admin_url( 'tools.php' ) );
} else {
// Full Complianz suite: page is registered under the main admin menu.
return add_query_arg( array( 'page' => 'terms-conditions' ), admin_url( 'admin.php' ) );
}
}
}
if ( ! function_exists( 'cmplz_tc_get_value' ) ) {
/**
* Retrieves a stored plugin option value for a given field name.
*
* Looks up the field definition in the config to determine which option group
* (source) the value belongs to, then reads it from the database. Falls back
* to the field's configured default when the stored value is absent and
* `$use_default` is true. When the field is marked as translatable, the value
* is passed through Polylang, WPML, or any registered `wpml_translate_single_string`
* filter before being returned.
*
* For very early usage (before the plugin classes are available), supply `$page`
* directly to bypass the config class lookup.
*
* @since 1.0.0
*
* @param string $fieldname The option field name (e.g. 'company_name').
* @param bool|string $page The option group/source identifier (e.g. 'settings'),
* or false to resolve it automatically from the field config.
* @param bool $use_default Whether to fall back to the field's configured default
* value when no stored value exists. Default true.
* @param bool $use_translate Whether to pass the value through multilingual
* translation plugins (Polylang, WPML). Default true.
* @return array|bool|mixed|string The stored (and optionally translated) value, the field
* default, an empty string, or false when the field is
* unknown and no page is supplied.
*/
function cmplz_tc_get_value( $fieldname, $page = false, $use_default = true, $use_translate = true ) {
// Return early when no page is given and the field is not registered in config.
if ( ! $page && ! isset( COMPLIANZ_TC::$config->fields[ $fieldname ] ) ) {
return false;
}
// Resolve the option source/group from the field definition when not supplied.
if ( ! $page ) {
$page = COMPLIANZ_TC::$config->fields[ $fieldname ]['source'];
}
$fields = get_option( 'complianz_tc_options_' . $page );
$default = ( $use_default && $page && isset( COMPLIANZ_TC::$config->fields[ $fieldname ]['default'] ) ) ? COMPLIANZ_TC::$config->fields[ $fieldname ]['default'] : '';
$value = isset( $fields[ $fieldname ] ) ? $fields[ $fieldname ] : $default;
// Pass translatable values through active multilingual plugins.
if ( $use_translate ) {
if ( isset( COMPLIANZ_TC::$config->fields[ $fieldname ]['translatable'] )
&& COMPLIANZ_TC::$config->fields[ $fieldname ]['translatable']
) {
if ( function_exists( 'pll__' ) ) {
$value = pll__( $value );
}
if ( function_exists( 'icl_translate' ) ) {
$value = icl_translate( 'complianz', $fieldname, $value );
}
/**
* Filters a translatable option value via WPML string translation.
*
* @since 1.0.0
*
* @param mixed $value The current field value.
* @param string $context The WPML context ('complianz').
* @param string $fieldname The field name being translated.
*/
$value = apply_filters( 'wpml_translate_single_string', $value, 'complianz', $fieldname );
}
}
return $value;
}
}
if ( ! function_exists( 'cmplz_tc_intro' ) ) {
/**
* Renders an introductory message panel in the plugin admin UI.
*
* Outputs a styled `<div>` panel containing the provided message. Used to
* display contextual introductions at the top of wizard steps or settings
* sections. Silently returns when `$msg` is empty.
*
* @since 1.0.0
*
* @param string $msg The message to display. HTML is allowed. Returns without
* output when this is an empty string.
* @return void
*/
function cmplz_tc_intro( $msg ) {
if ( '' === $msg ) {
return;
}
// Output the intro panel directly; $msg is expected to be pre-sanitised by the caller.
$html = "<div class='cmplz-panel cmplz-notification cmplz-intro'>{$msg}</div>";
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is pre-sanitised upstream.
}
}
if ( ! function_exists( 'cmplz_tc_notice' ) ) {
/**
* Renders or returns a notification panel for the plugin admin UI.
*
* Generates a styled notification `<div>` that can be echoed immediately or
* returned as a string for inclusion in a larger template. Supports conditional
* display: when `$condition` is provided, the panel is shown or hidden based on
* whether the specified wizard field currently matches the expected answer.
* The `$remove_after_change` flag adds a CSS class that hides the notice as soon
* as any wizard field value changes (handled by frontend JS).
*
* @since 1.0.0
*
* @param string $msg The notification message. HTML is allowed.
* Returns without output when empty.
* @param string $type Visual style of the notice. One of:
* 'notice' (default), 'warning', 'success'.
* @param bool $remove_after_change When true, adds the 'cmplz-remove-after-change'
* CSS class so JS hides the notice on field change.
* @param bool $do_echo When true (default) the HTML is echoed; when false
* it is returned as a string.
* @param array|bool $condition Optional condition array with keys:
* - 'question' (string) Field name to check.
* - 'answer' (string) Expected field value.
* The notice is hidden via CSS when the condition
* does not currently apply. Default false.
* @return string|void The HTML string when $do_echo is false, otherwise void.
*/
function cmplz_tc_notice( $msg, $type = 'notice', $remove_after_change = false, $do_echo = true, $condition = false ) {
if ( '' === $msg ) {
return;
}
// Build conditional visibility attributes when a condition is supplied.
$condition_check = '';
$condition_question = '';
$condition_answer = '';
$cmplz_hidden = '';
if ( $condition ) {
$condition_check = 'condition-check';
$condition_question = "data-condition-question='{$condition['question']}'";
$condition_answer = "data-condition-answer='{$condition['answer']}'";
$args['condition'] = array( $condition['question'] => $condition['answer'] );
// Check whether the condition currently applies to determine initial visibility.
$cmplz_hidden = cmplz_field::this()->condition_applies( $args ) ? '' : 'cmplz-hidden';
}
// Add the removal class when the notice should disappear on any field change.
$remove_after_change_class = $remove_after_change ? 'cmplz-remove-after-change' : '';
$html = "<div class='cmplz-panel-wrap'><div class='cmplz-panel cmplz-notification cmplz-{$type} {$remove_after_change_class} {$cmplz_hidden} {$condition_check}' {$condition_question} {$condition_answer}><div>{$msg}</div></div></div>";
if ( $do_echo ) {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is pre-sanitised upstream.
} else {
return $html;
}
}
}
if ( ! function_exists( 'cmplz_tc_sidebar_notice' ) ) {
/**
* Renders or returns a sidebar-style notification for the plugin admin UI.
*
* Similar to cmplz_tc_notice() but uses a different wrapper structure and CSS
* class intended for placement in the wizard or settings sidebar (help modals,
* contextual tips). Supports the same condition and remove-after-change behaviour.
*
* @since 1.0.0
*
* @see cmplz_tc_notice() For the main-content equivalent.
*
* @param string $msg The notification message. HTML is allowed.
* Returns without output when empty.
* @param string $type Visual style. One of: 'notice' (default),
* 'warning', 'success'.
* @param bool $remove_after_change When true, adds 'cmplz-remove-after-change'
* so the notice is hidden by JS on field change.
* @param bool $do_echo When true (default) the HTML is echoed; when false
* it is returned as a string.
* @param bool|array $condition Optional condition array with 'question' and
* 'answer' keys (see cmplz_tc_notice()). Default false.
* @return string|void The HTML string when $do_echo is false, otherwise void.
*/
function cmplz_tc_sidebar_notice( $msg, $type = 'notice', $remove_after_change = false, $do_echo = true, $condition = false ) {
if ( '' === $msg ) {
return;
}
// Build conditional visibility attributes when a condition is supplied.
$condition_check = '';
$condition_question = '';
$condition_answer = '';
$cmplz_hidden = '';
if ( $condition ) {
$condition_check = 'condition-check';
$condition_question = "data-condition-question='{$condition['question']}'";
$condition_answer = "data-condition-answer='{$condition['answer']}'";
$args['condition'] = array( $condition['question'] => $condition['answer'] );
// Check whether the condition currently applies to determine initial visibility.
$cmplz_hidden = cmplz_field::this()->condition_applies( $args ) ? '' : 'cmplz-hidden';
}
// Add the removal class when the notice should disappear on any field change.
$remove_after_change_class = $remove_after_change ? 'cmplz-remove-after-change' : '';
$html = "<div class='cmplz-help-modal cmplz-notice cmplz-{$type} {$remove_after_change_class} {$cmplz_hidden} {$condition_check}' {$condition_question} {$condition_answer}>{$msg}</div>";
if ( $do_echo ) {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is pre-sanitised upstream.
} else {
return $html;
}
}
}
if ( ! function_exists( 'cmplz_tc_localize_date' ) ) {
/**
* Translates the month name and weekday name within a date string to the current locale.
*
* Extracts the English month name (e.g. "June") and weekday name (e.g. "Wednesday")
* from the supplied date string using PHP's date() function, then substitutes each
* with its localised equivalent via WordPress's __() translation function.
*
* @since 1.0.0
*
* @param string $date A date string parseable by strtotime() (e.g. '2024-06-12').
* @return string The date string with the month and weekday names translated to the
* current WordPress locale.
*/
function cmplz_tc_localize_date( $date ) {
// Extract the English month name and replace it with the localised equivalent.
$month = gmdate( 'F', strtotime( $date ) ); // e.g. "June".
$month_localized = __( $month ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Variable contains an English month name produced by gmdate(); runtime translation is intentional.
$date = str_replace( $month, $month_localized, $date );
// Extract the English weekday name and replace it with the localised equivalent.
$weekday = gmdate( 'l', strtotime( $date ) ); // e.g. "Wednesday".
$weekday_localized = __( $weekday ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Variable contains an English weekday name produced by gmdate(); runtime translation is intentional.
$date = str_replace( $weekday, $weekday_localized, $date );
return $date;
}
}
if ( ! function_exists( 'cmplz_tc_read_more' ) ) {
/**
* Builds a localised "Read more" sentence with a hyperlink to an external article.
*
* Returns a formatted string like " For more information on this subject, please
* read this [article]." with `$url` as the link target. Optionally prepends a
* non-breaking space to visually separate it from preceding text.
*
* @since 1.0.0
*
* @param string $url The URL of the article or documentation page to link to.
* @param bool $add_space Whether to prepend a non-breaking space (`&nbsp;`) before
* the sentence. Default true.
* @return string The complete "read more" sentence HTML.
*/
function cmplz_tc_read_more( $url, $add_space = true ) {
$html
= sprintf(
// translators: %1$s is the opening anchor tag, %2$s is the closing anchor tag.
__(
'For more information on this subject, please read this %1$sarticle%2$s.',
'complianz-terms-conditions'
),
'<a target="_blank" href="' . $url . '">',
'</a>'
);
if ( $add_space ) {
$html = '&nbsp;' . $html;
}
return $html;
}
}
if ( ! function_exists( 'cmplz_tc_get_regions' ) ) {
/**
* Returns the list of geographical regions supported by the plugin.
*
* Currently the plugin generates a single Terms & Conditions document covering
* all regions. This function provides the region list used by the document and
* wizard configuration systems; future versions may expand it with region-specific
* entries.
*
* @since 1.0.0
*
* @return array Associative array of region slugs to display labels.
* Example: array( 'all' => 'All regions' )
*/
function cmplz_tc_get_regions() {
$output['all'] = __( 'All regions', 'complianz-terms-conditions' );
return $output;
}
}
// Register the activation hook before defining the callback so WordPress can bind it.
register_activation_hook( __FILE__, 'cmplz_tc_set_activation_time_stamp' );
if ( ! function_exists( 'cmplz_tc_set_activation_time_stamp' ) ) {
/**
* Stores the plugin activation timestamp as a WordPress option.
*
* Fired on plugin activation via register_activation_hook(). The timestamp is
* used by other parts of the plugin to determine how long the plugin has been
* installed (e.g. for delaying review prompts or deferred notices).
*
* @since 1.0.0
*
* @param bool $networkwide True when activated network-wide on a multisite install,
* false for a single-site activation.
* @return void
*/
function cmplz_tc_set_activation_time_stamp( $networkwide ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required by register_activation_hook() signature; multisite support may use it in future.
update_option( 'cmplz_tc_activation_time', time() );
}
}
if ( ! function_exists( 'cmplz_tc_allowed_html' ) ) {
/**
* Returns the list of HTML tags and attributes permitted for wp_kses() sanitisation.
*
* Defines an extended allowed-tag map used when sanitising plugin-generated HTML
* that may contain rich markup (e.g. wizard notices, document content). The list
* can be extended by third parties via the `cmplz_tc_allowed_html` filter.
*
* @since 1.0.0
*
* @return array Associative array of allowed HTML tags mapped to their permitted
* attributes. Format matches the $allowed_html parameter of wp_kses().
*/
function cmplz_tc_allowed_html() {
// Define the full set of tags and attributes the plugin may output.
$allowed_tags = array(
'a' => array(
'class' => array(),
'href' => array(),
'rel' => array(),
'title' => array(),
'target' => array(),
'id' => array(),
),
'button' => array(
'id' => array(),
'class' => array(),
'href' => array(),
'rel' => array(),
'title' => array(),
'target' => array(),
),
'b' => array(),
'br' => array(),
'blockquote' => array(
'cite' => array(),
),
'div' => array(
'class' => array(),
'id' => array(),
),
'h1' => array(),
'h2' => array(),
'h3' => array(),
'h4' => array(),
'h5' => array(),
'h6' => array(),
'i' => array(),
'input' => array(
'type' => array(),
'class' => array(),
'id' => array(),
'required' => array(),
'value' => array(),
'placeholder' => array(),
'data-category' => array(),
'style' => array(
'color' => array(),
),
),
'img' => array(
'alt' => array(),
'class' => array(),
'height' => array(),
'src' => array(),
'width' => array(),
),
'label' => array(
'for' => array(),
'class' => array(),
'style' => array(
'visibility' => array(),
),
),
'li' => array(
'class' => array(),
'id' => array(),
),
'ol' => array(
'class' => array(),
'id' => array(),
),
'p' => array(
'class' => array(),
'id' => array(),
),
'span' => array(
'class' => array(),
'title' => array(),
'style' => array(
'color' => array(),
'display' => array(),
),
'id' => array(),
),
'strong' => array(),
'table' => array(
'class' => array(),
'id' => array(),
),
'tr' => array(),
// SVG elements are included to support inline icon output.
'svg' => array(
'width' => array(),
'height' => array(),
'viewBox' => array(),
),
'polyline' => array(
'points' => array(),
),
'path' => array(
'd' => array(),
),
'style' => array(),
'td' => array(
'colspan' => array(),
'scope' => array(),
),
'th' => array( 'scope' => array() ),
'ul' => array(
'class' => array(),
'id' => array(),
),
);
/**
* Filters the allowed HTML tags and attributes for plugin output sanitisation.
*
* @since 1.0.0
*
* @param array $allowed_tags Associative array of tags to permitted attributes.
*/
return apply_filters( 'cmplz_tc_allowed_html', $allowed_tags );
}
}
if ( ! function_exists( 'cmplz_tc_translate' ) ) {
/**
* Translates a field value through active multilingual plugins.
*
* Passes the supplied value through Polylang (pll__()), WPML (icl_translate()),
* and the `wpml_translate_single_string` filter in sequence, returning the
* translated result. Only active translation plugins affect the value; inactive
* ones are silently skipped.
*
* @since 1.0.0
*
* @param string $value The original field value to translate.
* @param string $fieldname The field name used as the WPML string context identifier.
* @return string The translated value, or the original when no translation is found.
*/
function cmplz_tc_translate( $value, $fieldname ) {
if ( function_exists( 'pll__' ) ) {
$value = pll__( $value );
}
if ( function_exists( 'icl_translate' ) ) {
$value = icl_translate( 'complianz', $fieldname, $value );
}
/**
* Filters the translated value via WPML string translation.
*
* @since 1.0.0
*
* @param mixed $value The current field value.
* @param string $context The WPML context ('complianz').
* @param string $fieldname The field name being translated.
*/
$value = apply_filters( 'wpml_translate_single_string', $value, 'complianz', $fieldname );
return $value;
}
}
if ( ! function_exists( 'cmplz_tc_sanitize_language' ) ) {
/**
* Sanitises a language string to a two-letter ISO 639-1 code.
*
* Truncates the input to its first two characters, validates that they form a
* string of exactly two ASCII letters, and returns the code in lowercase. This
* prevents invalid or overly long locale strings (e.g. 'en_US') from being
* stored where a simple language code is expected.
*
* @since 1.0.0
*
* @param mixed $language The raw language value to sanitise (e.g. 'en', 'EN', 'en_US').
* @return bool|string Lowercase two-letter language code on success (e.g. 'en'),
* or false when the input is not a valid string or does not
* contain two ASCII letters.
*/
function cmplz_tc_sanitize_language( $language ) {
// Pattern matches exactly two ASCII letters (a-z or A-Z).
$pattern = '/^[a-zA-Z]{2}$/';
if ( ! is_string( $language ) ) {
return false;
}
// Truncate to two characters to handle locale strings like 'en_US'.
$language = substr( $language, 0, 2 );
if ( (bool) preg_match( $pattern, $language ) ) {
$language = strtolower( $language );
return $language;
}
return false;
}
}
if ( ! function_exists( 'cmplz_tcf_creative_commons' ) ) {
/**
* Callback: checks whether the Creative Commons document section should be shown.
*
* Used as a `callback_condition` in the document element configuration to
* conditionally include the Creative Commons licence paragraph. Returns false
* (hide the section) when the copyright setting is 'allrights' or 'norights',
* because those values indicate a standard copyright notice rather than a CC licence.
*
* @since 1.0.0
*
* @return bool True when a Creative Commons licence type is selected, false otherwise.
*/
function cmplz_tcf_creative_commons() {
$type = cmplz_tc_get_value( 'about_copyright' );
if ( 'allrights' === $type || 'norights' === $type ) {
return false;
} else {
return true;
}
}
}
if ( ! function_exists( 'cmplz_tcf_nuts' ) ) {
/**
* Callback: checks whether the NUTS (Network Utility Transaction Services) section applies.
*
* Used as a `callback_condition` for document sections that are specific to
* utility or service subscriptions sold via NUTS. Returns true when the return
* policy covers NUTS services or utilities, so that the relevant withdrawal
* provisions are included in the generated document.
*
* @since 1.0.0
*
* @return bool True when the return type is 'nuts_services' or 'nuts_utilities',
* false otherwise.
*/
function cmplz_tcf_nuts() {
$services = cmplz_tc_get_value( 'about_returns' ) === 'nuts_services';
$utilities = cmplz_tc_get_value( 'about_returns' ) === 'nuts_utilities';
if ( $services || $utilities ) {
return true;
} else {
return false;
}
}
}
if ( ! function_exists( 'cmplz_tc_uses_gutenberg' ) ) {
/**
* Checks whether the current WordPress install is using the Gutenberg block editor.
*
* Returns true when the has_block() function exists (WordPress 5.0+) and the
* Classic Editor plugin is not active. This is used to decide whether to register
* a Gutenberg block or fall back to a classic shortcode widget.
*
* @since 1.0.0
*
* @return bool True when Gutenberg is available and Classic Editor is not active,
* false otherwise.
*/
function cmplz_tc_uses_gutenberg() {
if ( function_exists( 'has_block' )
&& ! class_exists( 'Classic_Editor' )
) {
return true;
}
return false;
}
}
if ( ! function_exists( 'cmplz_tc_user_can_manage' ) ) {
/**
* Checks whether the current user has permission to manage plugin settings.
*
* Used as a capability gate throughout the plugin's admin UI. Requires both an
* active login session and the `manage_options` capability (typically limited to
* administrators). Returns false immediately when either condition is not met.
*
* @since 1.0.0
*
* @return bool True if the current user is logged in and has `manage_options`,
* false otherwise.
*/
function cmplz_tc_user_can_manage() {
if ( ! is_user_logged_in() ) {
return false;
}
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
return true;
}
}
if ( ! function_exists( 'cmplz_tc_array_filter_multidimensional' ) ) {
/**
* Filters a multidimensional array, keeping only elements where a specific key matches a value.
*
* Iterates over a flat array of associative sub-arrays and returns only those whose
* `$filter_key` element equals `$filter_value`. Elements that do not have the
* specified key are excluded. This is used by the wizard to extract fields that
* match a particular attribute, such as all required fields (`'required' => true`).
*
* @since 1.0.0
*
* @param array $items The input array of associative sub-arrays to filter.
* @param string $filter_key The key to check within each sub-array.
* @param mixed $filter_value The value that $filter_key must equal for an element
* to be included in the result.
* @return array Filtered array preserving original keys, containing only
* elements where $items[n][$filter_key] === $filter_value.
*/
function cmplz_tc_array_filter_multidimensional(
$items,
$filter_key,
$filter_value
) {
$new = array_filter(
$items,
function ( $element ) use ( $filter_value, $filter_key ) {
return isset( $element[ $filter_key ] ) ? ( $element[ $filter_key ] === $filter_value ) : false;
}
);
return $new;
}
}