PluginProbe
Code Snippets / 3.10.2
Code Snippets v3.10.2
3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 All 64 releases
code-snippets / php / Model / Model.php

Model.php in Code Snippets 3.10.2, at php/Model/Model.php

241 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Model;
4
5 use WP_Exception;
6
7 /**
8 * Base class for representing an item of data without needing to use direct access or individual getter and setter functions.
9 *
10 * @package Code_Snippets
11 */
12 abstract class Model {
13
14 /**
15 * List of data fields keyed to their current values. Will be initialised with default values.
16 *
17 * @var array<string, mixed>
18 */
19 protected array $fields;
20
21 /**
22 * List of default values provided for fields.
23 *
24 * @var array<string, mixed>
25 */
26 protected static array $default_values = [];
27
28 /**
29 * Optional list of field name aliases to map when resolving a field name.
30 *
31 * @var array<string, string> Field alias names keyed to actual field names.
32 */
33 protected static array $field_aliases = [];
34
35 /**
36 * Class constructor.
37 *
38 * @param array<string, mixed>|Model $initial_data Optional initial data to populate fields.
39 */
40 public function __construct( $initial_data = null ) {
41 assert( static::$default_values, get_class( $this ) . '::$default_values not set' );
42 $this->fields = static::$default_values;
43 $this->set_fields( $initial_data );
44 }
45
46 /**
47 * Set all data fields from an array or object. Invalid fields will be ignored.
48 *
49 * @param array<string, mixed>|Model|mixed $data List of data.
50 */
51 public function set_fields( $data ) {
52 // Only accept arrays or objects.
53 if ( ! is_array( $data ) && ! is_object( $data ) ) {
54 return;
55 }
56
57 // Convert objects into arrays.
58 if ( is_object( $data ) ) {
59 $data = method_exists( $data, 'get_fields' )
60 ? $data->get_fields()
61 : get_object_vars( $data );
62 }
63
64 // Loop through the provided fields and set their values.
65 foreach ( $data as $field => $value ) {
66 $this->set_field( $field, $value );
67 }
68 }
69
70 /**
71 * Retrieve list of current data fields.
72 *
73 * @return array<string, mixed> Field names keyed to current values.
74 */
75 public function get_fields(): array {
76 $fields = [];
77
78 foreach ( $this->get_allowed_fields() as $field_name ) {
79 $fields[ $field_name ] = $this->$field_name;
80 }
81
82 return $fields;
83 }
84
85 /**
86 * Retrieve a list of current data fields, excluding values that are unchanged from the default.
87 *
88 * @return array<string, mixed>
89 */
90 public function get_modified_fields(): array {
91 return array_filter(
92 $this->get_fields(),
93 function ( $value, $field ) {
94 // The field list includes aliases, which have no entry of their own
95 // in the defaults. Compare against the field an alias resolves to,
96 // so reading a snippet built from alias keys does not warn.
97 $default = static::$default_values[ static::resolve_field_name( $field ) ] ?? null;
98 return $value && $value !== $default;
99 },
100 ARRAY_FILTER_USE_BOTH
101 );
102 }
103
104 /**
105 * Internal function for resolving the actual name of a field.
106 *
107 * @param string $field A field name, potentially a field alias.
108 *
109 * @return string The resolved field name.
110 */
111 protected static function resolve_field_name( string $field ): string {
112 return static::$field_aliases[ $field ] ?? $field;
113 }
114
115 /**
116 * Check if a field is set.
117 *
118 * @param string $field The field name.
119 *
120 * @return bool Whether the field is set.
121 */
122 public function __isset( string $field ) {
123 $field = self::resolve_field_name( $field );
124 return isset( $this->fields[ $field ] ) || method_exists( $this, 'get_' . $field );
125 }
126
127 /**
128 * Retrieve a field's value.
129 *
130 * @param string $field The field name.
131 *
132 * @return mixed The field value
133 *
134 * @throws WP_Exception If the field name is not allowed.
135 */
136 public function __get( string $field ) {
137 $field = self::resolve_field_name( $field );
138
139 if ( method_exists( $this, 'get_' . $field ) ) {
140 return call_user_func( array( $this, 'get_' . $field ) );
141 }
142
143 if ( ! $this->is_allowed_field( $field ) ) {
144 if ( function_exists( 'wp_trigger_error' ) ) {
145 // translators: 1: class name, 2: field name.
146 $message = sprintf( 'Trying to access invalid property on "%1$s" class: %2$s', get_class( $this ), $field );
147 wp_trigger_error( __FUNCTION__, $message, E_USER_WARNING );
148 }
149
150 return null;
151 }
152
153 return $this->fields[ $field ];
154 }
155
156 /**
157 * Set the value of a field without any validation.
158 *
159 * @param string $resolved_field The resolved field name.
160 * @param mixed $value The field value.
161 */
162 private function set_value_internal( string $resolved_field, $value ) {
163 $value = method_exists( $this, 'prepare_' . $resolved_field ) ?
164 call_user_func( array( $this, 'prepare_' . $resolved_field ), $value ) :
165 $this->prepare_field( $value, $resolved_field );
166
167 $this->fields[ $resolved_field ] = $value;
168 }
169
170 /**
171 * Set the value of a field.
172 *
173 * @param string $field The field name.
174 * @param mixed $value The field value.
175 *
176 * @throws WP_Exception If the field name is not allowed.
177 */
178 public function __set( string $field, $value ) {
179 $resolved_field = $this->resolve_field_name( $field );
180
181 if ( $this->is_allowed_field( $resolved_field ) ) {
182 $this->set_value_internal( $resolved_field, $value );
183 } elseif ( function_exists( 'wp_trigger_error' ) ) {
184 // translators: 1: class name, 2: field name.
185 $message = sprintf( 'Trying to set invalid property on "%s" class: %s', get_class( $this ), $field );
186 wp_trigger_error( __FUNCTION__, $message, E_USER_ERROR );
187 }
188 }
189
190 /**
191 * Prepare a value before it is stored.
192 *
193 * @param mixed $value Value to prepare.
194 * @param string $field Field name.
195 *
196 * @return mixed Value in the correct format.
197 */
198 abstract protected function prepare_field( $value, string $field );
199
200 /**
201 * Retrieve the list of fields that can be written to.
202 *
203 * @return array<string> List of field names.
204 */
205 public function get_allowed_fields(): array {
206 return array_merge( array_keys( $this->fields ), array_keys( static::$field_aliases ) );
207 }
208
209 /**
210 * Determine whether a field is allowed to be written to
211 *
212 * @param string $field The field name.
213 *
214 * @return bool true if the is allowed, false if invalid.
215 */
216 public function is_allowed_field( string $field ): bool {
217 return ( $this->fields && array_key_exists( $field, $this->fields ) ) ||
218 ( static::$field_aliases && array_key_exists( $field, static::$field_aliases ) );
219 }
220
221 /**
222 * Safely set the value for a field.
223 * If the field name is invalid, false will be returned instead of an error thrown.
224 *
225 * @param string $field The field name.
226 * @param mixed $value The field value.
227 *
228 * @return bool true if the field was set successfully, false if the field name is invalid.
229 */
230 public function set_field( string $field, $value ): bool {
231 $resolved_field = $this->resolve_field_name( $field );
232
233 if ( ! $this->is_allowed_field( $resolved_field ) ) {
234 return false;
235 }
236
237 $this->set_value_internal( $resolved_field, $value );
238 return true;
239 }
240 }
241