JezK
Edit File: DTO.php
<?php /** * Base data transfer object with array construction, request hydration, and JSON serialization. * Supports attribute casting, field picking/exclusion, and meta field extraction. * Bridges validated HTTP input and API output with a typed property bag. * * @package Framework * @since 1.0.0 */ namespace Kirki\Framework; \defined('ABSPATH') || exit; use Kirki\Framework\Contracts\CastAttribute; use Kirki\Framework\Contracts\Request; use Kirki\Framework\Contracts\Support\Arrayable; use Kirki\Framework\Exceptions\ValidationException; use Exception; use JsonSerializable; class DTO implements JsonSerializable, Arrayable { /** * Fields that are considered not part of "meta" data. * * @var array * * @since 1.0.0 */ protected static $base_fields = ['id', 'title', 'description', 'slug']; /** * Fields that are considered to cast data when serialized. * * @var array * * @since 1.0.0 */ protected $casts = []; /** * Tracks which attributes have been prepared for display * * @var bool * * @since 1.0.0 */ protected $prepare_for_display = \false; /** * Fields to exclude from public attributes. * * @var array * * @since 1.0.0 */ protected array $excluded_keys = []; /** * Fields to pick from public attributes. * * @var array * * @since 1.0.0 */ protected array $picked_keys = []; /** * Dto constructor. * * @param array $data The data payload. * * @return void * * @since 1.0.0 */ public function __construct(array $data = []) { foreach ($data as $key => $value) { if (\property_exists($this, $key)) { $this->{$key} = $value; } } } /** * Create DTO from array * * @param array $data The data payload. * * @return static * * @since 1.0.0 */ public static function from_array(array $data) { return new static($data); } /** * Create DTO from Request * * @param Request $request The request instance. * * @return static * * @since 1.0.0 */ public static function from_request(Request $request) { $request->validated(); $data = $request->sanitized(); return static::from_array($data); } /** * Create DTO from list * * @param array $items The items. * * @return static[] * * @since 1.0.0 */ public static function from_list(array $items) { $dtos = \array_map(function ($item) { return static::from_array($item); }, $items); return $dtos; } /** * Return an array representation of the object * * @return array * * @since 1.0.0 */ public function jsonSerialize() : array { $this->cast_attributes(); return $this->to_array(); } /** * Convert DTO to array * * @return array * * @since 1.0.0 */ public function to_array() { if (!empty($this->excluded_keys)) { return $this->except($this->excluded_keys); } if (!empty($this->picked_keys)) { return $this->only($this->picked_keys); } return $this->all(); } /** * Extract metadata fields only * * @param array $except The except. * * @return array * * @since 1.0.0 */ public function get_meta(array $except = []) { $fields = $this->all(); $meta = []; $fields_to_skip = \array_merge(static::$base_fields, $except); foreach ($fields as $key => $value) { if (!\in_array($key, $fields_to_skip, \true)) { $meta[$key] = $value; } } return $meta; } /** * Get all fields * * @return array * * @since 1.0.0 */ public function all() { $data = $this->get_public_vars(); return $data; } /** * Get all excluded fields * * @param array $keys The keys. * * @return array * * @since 1.0.0 */ public function except(array $keys) { $data = $this->all(); return \array_diff_key($data, \array_flip($keys)); } /** * Set all excluded fields * * @param array $keys The keys. * * @return static * * @since 1.0.0 */ public function exclude(array $keys) { $this->excluded_keys = $keys; return $this; } /** * Get all only included fields * * @param array $keys The keys. * * @return array * * @since 1.0.0 */ public function only(array $keys) { $data = $this->all(); return \array_intersect_key($data, \array_flip($keys)); } /** * Set all included fields * * @param array $keys The keys. * * @return static * * @since 1.0.0 */ public function pick(array $keys) { $this->picked_keys = $keys; return $this; } /** * Get public properties with values * * @return array * * @since 1.0.0 */ protected function get_public_vars() : array { $vars = []; $reflection = new \ReflectionClass($this); foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); $vars[$name] = $this->{$name}; } return $vars; } /** * Get casts * * @return array * * @since 1.0.0 */ protected function get_casts() { return $this->casts; } /** * Cast all attributes * * @return $this * * @since 1.0.0 */ protected function cast_attributes() { if ($this->prepare_for_display) { return $this; } foreach ($this->get_casts() as $key => $cast) { $field = \explode('.', $key); $attribute = $field[0]; if (\property_exists($this, $attribute)) { \array_shift($field); $this->{$attribute} = $this->traverse_and_cast_attribute($this->{$attribute}, $field, $cast); } } // Mark as prepared for display $this->prepare_for_display = \true; return $this; } /** * Traverse and cast attribute * * @param mixed $current_field_value The current field value. * @param array $key_segments The key segments. * @param mixed $cast The cast. * * @return mixed * * @throws \Exception * * @since 1.0.0 */ protected function traverse_and_cast_attribute($current_field_value, array $key_segments, $cast) { if (empty($key_segments)) { if (\is_subclass_of($cast, CastAttribute::class)) { $attribute_class = new $cast(); return $attribute_class->get($current_field_value, $current_field_value); } elseif (\is_callable($cast)) { return $cast(); } throw new Exception('Cast must be an instance of ' . CastAttribute::class . ' or a callable'); } $segment = \array_shift($key_segments); if ($segment === '*') { if (!\is_array($current_field_value)) { return $current_field_value; } foreach ($current_field_value as $key => $value) { $current_field_value[$key] = $this->traverse_and_cast_attribute($value, $key_segments, $cast); } } elseif (\is_array($current_field_value) && \array_key_exists($segment, $current_field_value)) { $current_field_value[$segment] = $this->traverse_and_cast_attribute($current_field_value[$segment], $key_segments, $cast); } elseif (\is_object($current_field_value) && \property_exists($current_field_value, $segment)) { $current_field_value->{$segment} = $this->traverse_and_cast_attribute($current_field_value->{$segment}, $key_segments, $cast); } return $current_field_value; } /** * Get the values. * * @return mixed * * @since 1.0.0 */ public function get_values() { return $this->cast_attributes(); } }