PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / schema-aggregator / application / properties-merger.php

properties-merger.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at src/schema-aggregator/application/properties-merger.php

171 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4 namespace Yoast\WP\SEO\Schema_Aggregator\Application;
5
6 use Yoast\WP\SEO\Schema_Aggregator\Domain\Schema_Piece;
7
8 /**
9 * Merges properties of two schema pieces with the same @id.
10 */
11 class Properties_Merger {
12
13 /**
14 * Merges two Schema_Pieces into one by merging their properties.
15 *
16 * @param Schema_Piece $piece1 First schema piece.
17 * @param Schema_Piece $piece2 Second schema piece.
18 *
19 * @return Schema_Piece Merged schema piece.
20 */
21 public function merge( Schema_Piece $piece1, Schema_Piece $piece2 ): Schema_Piece {
22 $merged_properties = $this->merge_properties( $piece1->get_data(), $piece2->get_data() );
23 // TODO: Shall we check if $type !== null?
24 return new Schema_Piece( $merged_properties, $merged_properties['@type'] );
25 }
26
27 /**
28 * Merge properties from two schema entities with the same @id
29 *
30 * Strategy:
31 * - @type: Special handling - merge types into unified array
32 * - @id: Skip (always the same)
33 * - Arrays: Combine unique values
34 * - Scalars: Prefer non-empty over empty
35 * - Objects: Deep merge recursively
36 * - Null vs value: Prefer non-null
37 *
38 * @param array<string, string|int|bool> $entity1 First entity.
39 * @param array<string, string|int|bool> $entity2 Second entity.
40 *
41 * @return array<string, string|int|bool> Merged entity.
42 */
43 private function merge_properties( array $entity1, array $entity2 ): array {
44 $merged = $entity1;
45
46 foreach ( $entity2 as $key => $value ) {
47
48 if ( $key === '@id' ) {
49 continue;
50 }
51
52 // Special handling for @type - merge types (JSON-LD allows multiple types).
53 if ( $key === '@type' ) {
54 $merged['@type'] = $this->merge_types(
55 ( $merged['@type'] ?? null ),
56 $value,
57 );
58 continue;
59 }
60
61 if ( ! isset( $merged[ $key ] ) || $merged[ $key ] === '' ) {
62
63 $merged[ $key ] = $value;
64 }
65 elseif ( \is_array( $merged[ $key ] ) && \is_array( $value ) ) {
66 // Both are arrays - check if associative (object) or indexed (list).
67 if ( $this->is_associative_array( $merged[ $key ] ) || $this->is_associative_array( $value ) ) {
68 // Deep merge objects.
69 $merged[ $key ] = $this->merge_properties( $merged[ $key ], $value );
70 }
71 else {
72 // Combine arrays and get unique values.
73 $merged[ $key ] = \array_values( \array_unique( \array_merge( $merged[ $key ], $value ), \SORT_REGULAR ) );
74 }
75 }
76 // Else: entity1's value is non-empty scalar, keep it (prefer first occurrence).
77 }
78
79 return $merged;
80 }
81
82 /**
83 * Merge @type values from two entities
84 *
85 * JSON-LD allows @type to be either a string or an array of strings.
86 * This method combines types from both entities, deduplicates them,
87 * and normalizes the result (string if 1 type, array if multiple).
88 *
89 * Examples:
90 * - merge_types("Person", "Person") → "Person"
91 * - merge_types("Person", "Author") → ["Person", "Author"]
92 * - merge_types("Person", ["Author", "Employee"]) → ["Person", "Author", "Employee"]
93 * - merge_types(["Person"], "Person") → "Person"
94 * - merge_types(["Person", "Author"], ["Author", "Employee"]) → ["Person", "Author", "Employee"]
95 *
96 * @param string|array<string>|null $type1 First @type value.
97 * @param string|array<string>|null $type2 Second @type value.
98 * @return string|array<string> Merged and normalized @type value.
99 */
100 private function merge_types( $type1, $type2 ) {
101
102 $types1 = $this->normalize_type_to_array( $type1 );
103 $types2 = $this->normalize_type_to_array( $type2 );
104
105 $merged = \array_unique( \array_merge( $types1, $types2 ), \SORT_REGULAR );
106
107 return $this->normalize_type_from_array( $merged );
108 }
109
110 /**
111 * Normalize @type value to array format
112 *
113 * @param string|array<string>|null $type Type value to normalize.
114 * @return array<string> Array of type strings.
115 */
116 private function normalize_type_to_array( $type ): array {
117 if ( $type === null ) {
118 return [];
119 }
120
121 if ( \is_string( $type ) ) {
122 return [ $type ];
123 }
124
125 if ( \is_array( $type ) ) {
126
127 return \array_values( \array_filter( $type, 'is_string' ) );
128 }
129
130 return [];
131 }
132
133 /**
134 * Normalize array of types back to string or array.
135 *
136 * Returns string if single type, array if multiple types.
137 * This keeps the output compact while supporting multi-type entities.
138 *
139 * @param array<string> $types Array of type strings.
140 * @return string|array<string> Normalized type value.
141 */
142 private function normalize_type_from_array( array $types ) {
143 // Remove duplicates and re-index.
144 $types = \array_values( \array_unique( $types ) );
145
146 if ( empty( $types ) ) {
147 // Fallback - should not happen in normal flow.
148 return 'Thing'; // schema.org root type.
149 }
150
151 if ( \count( $types ) === 1 ) {
152 return $types[0];
153 }
154
155 return $types;
156 }
157
158 /**
159 * Check if array is associative (object-like) vs indexed (list-like)
160 *
161 * @param array<string|int, string|int|bool|array<string|int|bool>> $argument Array to check.
162 * @return bool True if associative.
163 */
164 private function is_associative_array( array $argument ): bool {
165 if ( empty( $argument ) ) {
166 return false;
167 }
168 return \array_keys( $argument ) !== \range( 0, ( \count( $argument ) - 1 ) );
169 }
170 }
171