Logo of the site

NomadDesk

A Recursive Approach to Argument Parsing in PHP

avatar

Przemysław Spławski

Context and Purpose of parse_args function

This function was specifically crafted for our PHP In-House framework, which draws inspiration from React's component and prop system. Much like React components receive props, our framework's components accept various arguments that dictate their behavior and appearance. The parse_args function was designed to handle the parsing of these props in a way that's both efficient and flexible, allowing for deep merging of default and provided props, and the ability to selectively remove certain arguments post-merge. This advanced functionality enhances our framework's capability to manage component arguments in a nuanced and sophisticated manner, akin to React's own props system but tailored to the PHP environment.

A Recursive Approach to Argument Parsing in PHP

When working with WordPress, or PHP in general, handling arrays of arguments efficiently can significantly impact the clarity and flexibility of your code. WordPress offers a handy function called wp_parse_args() for merging user-defined arguments into defaults. However, this function doesn't support recursive parsing and merging of arrays. This is where our custom parse_args function comes into play, enhancing the capabilities of argument parsing to a new level.

What parse_args Does Differently

Before diving into the code, let's highlight what makes parse_args stand out:

Understanding the Function Step by Step

Let's break down the function into smaller chunks to understand how it operates:

Initial Checks and Setup

1if ( ! is_array( $unsets ) ) {
2 $unsets = [];
3}
4
5if ( ! is_array( $arguments ) ) {
6 if ( ! is_array( $defaults ) || empty( $defaults ) ) {
7 return [];
8 } else {
9 return $defaults;
10 }
11} elseif ( ! is_array( $defaults ) ) {
12 return $arguments;
13}

This section ensures that both $unsets and $arguments are arrays. If not, it sets up $unsets as an empty array and returns early in cases where either $arguments or $defaults isn't an array, ensuring type safety and avoiding further unnecessary processing.

Pre-filling and Numeric Array Handling

1$parsed = $defaults;
2$both_arrays_numeric = wp_is_numeric_array( $arguments ) && wp_is_numeric_array( $defaults );
3$prepends = [];

Here, the function prepares the groundwork for merging by setting $parsed as $defaults and checking if both $arguments and $defaults are numeric arrays, which affects how values are merged.

Main Merging Logic

1foreach ( $arguments as $key => $value ) {
2 if ( ! isset( $parsed[ $key ] ) ) {
3 $parsed[ $key ] = $value;
4 continue;
5 }
6
7 if ( ! $both_arrays_numeric && is_array( $value ) && is_array( $parsed[ $key ] ) ) {
8 $unset = $unsets[ $key ] ?? [];
9 $parsed[ $key ] = parse_args( $value, $parsed[ $key ], $unset );
10 } elseif ( $both_arrays_numeric ) {
11 $prepends[] = $value;
12 } else {
13 $parsed[ $key ] = $value;
14 }
15}

This chunk is the heart of the function, iterating over each argument. It decides whether to merge arrays recursively, prepend values if both arrays are numeric, or simply replace the default with the argument value.

Handling Prepends and Unsets

1if ( ! empty( $prepends ) ) {
2 array_unshift( $parsed, ...$prepends );
3}
4
5foreach ( $unsets as $unset ) {
6 if ( isset( $parsed[ $unset ] ) ) {
7 unset( $parsed[ $unset ] );
8 }
9}

After merging, any values meant to be prepended (in cases of numeric arrays) are added to the beginning. Then, it processes $unsets to remove specified keys from the final array.

Final Adjustments for Numeric Arrays

1if ( $both_arrays_numeric && ! empty( $unsets ) ) {
2 $parsed = array_values( $parsed );
3}

For numeric arrays, this ensures that the final array is re-indexed, preserving the sequential index order, especially after unset operations.

Comparison with wp_parse_args

The built-in wp_parse_args function is great for basic argument merging but falls short when dealing with complex or multi-dimensional arrays. Our parse_args function excels by:

The Complete Function

Here's the complete code for parse_args for your reference:

1/**
2 * Parses arguments against default values RECURSIVELY (means: preserves arrays).
3 *
4 * @param mixed $arguments some arguments.
5 * @param mixed $defaults default arguments list.
6 * @param mixed $unsets arguments that should be unset from after the merge.
7 *
8 * @return array of arguments with defaults filled in.
9 */
10function parse_args( $arguments, $defaults, $unsets = [] ): array {
11 if ( ! is_array( $unsets ) ) {
12 // Smart.
13 $unsets = [];
14 }
15
16 if ( ! is_array( $arguments ) ) {
17 if ( ! is_array( $defaults ) || empty( $defaults ) ) {
18 return [];
19 } else {
20 return $defaults;
21 }
22 } elseif ( ! is_array( $defaults ) ) {
23 if ( empty( $arguments ) && empty( $defaults ) ) {
24 return $arguments;
25 } elseif ( empty( $arguments ) && is_array( $defaults ) ) {
26 return $defaults;
27 } else {
28 return [];
29 }
30 return $arguments;
31 }
32
33 // Pre-fill the parsed values with defaults, so we can easily overwrite every property.
34 $parsed = $defaults;
35 $both_arrays_numeric = wp_is_numeric_array( $arguments ) && wp_is_numeric_array( $defaults );
36 $prepends = [];
37
38 foreach ( $arguments as $key => $value ) {
39 if ( ! isset( $parsed[ $key ] ) ) {
40 $parsed[ $key ] = $value;
41 continue;
42 }
43
44 if ( ! $both_arrays_numeric && is_array( $value ) && is_array( $parsed[ $key ] ) ) {
45 // The passed argument in this place is an array and so is the default one, so we need to go one level deeper.
46 $unset = $unsets[ $key ] ?? [];
47 $parsed[ $key ] = parse_args( $value, $parsed[ $key ], $unset );
48 } elseif ( $both_arrays_numeric ) {
49 $prepends[] = $value;
50 } else {
51 $parsed[ $key ] = $value;
52 }
53 }
54
55 if ( ! empty( $prepends ) ) {
56 array_unshift( $parsed, ...$prepends );
57 }
58
59 foreach ( $unsets as $unset ) {
60 if ( is_array( $unset ) ) {
61 // This has been done in the previous iteration of the loop, when the parse_args was invoked recursively.
62 continue;
63 }
64
65 if ( $both_arrays_numeric && ! is_int( $unset ) ) {
66 $index = array_search( $unset, $parsed, true );
67 if ( $index ) {
68 unset( $parsed[ $index ] );
69 }
70 } elseif ( isset( $parsed[ $unset ] ) ) {
71 unset( $parsed[ $unset ] );
72 }
73 }
74
75 if ( $both_arrays_numeric && ! empty( $unsets ) ) {
76 $parsed = array_values( $parsed );
77 }
78
79 return $parsed;
80}

Conclusion

In scenarios requiring nuanced control over argument arrays, especially when dealing with nested structures, parse_args offers a robust solution beyond the capabilities of wp_parse_args. It's a testament to the flexibility PHP offers, allowing developers to extend or enhance existing functionalities to meet their specific project needs.