| | |
| | | <?php |
| | | namespace JVBase\meta; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Main facade for meta operations |
| | | * Fluent API for getting/setting meta values with validation & sanitization |
| | | * |
| | | * Usage: |
| | | * $meta = Meta::forPost($id); |
| | | * $meta->price = 150; |
| | | * $meta->save(); |
| | | * |
| | | * Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save(); |
| | | */ |
| | | class Meta |
| | | { |
| | | /** |
| | | * @var string post, term, user, or options |
| | | */ |
| | | protected string $type; |
| | | /** |
| | | * @var ?string the full slug, with BASE |
| | | */ |
| | | protected ?string $slug; |
| | | |
| | | protected string $contentType; |
| | | protected Item $item; |
| | | protected Storage $storage; |
| | | protected Validator $validator; |
| | | protected Sanitizer $sanitizer; |
| | | protected array $fields; |
| | | protected WP_Post|WP_Term|WP_User|false|null $wpObject; |
| | | protected int|string $ID; |
| | | protected MetaTypeManager $typeManager; |
| | | protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]]; |
| | | |
| | | protected bool $autoValidate = true; |
| | | protected bool $autoSanitize = true; |
| | | |
| | | /** @var array<string, callable[]> */ |
| | | protected array $onChangeCallbacks = []; |
| | | |
| | | /** @var array<string, callable> */ |
| | | protected array $computed = []; |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Factory Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | protected array $defaults = ['post_thumbnail']; |
| | | /** |
| | | * Create Meta instance for a post |
| | | */ |
| | | public static function forPost(int $id): self |
| | | { |
| | | return new self($id, 'post'); |
| | | if (array_key_exists($id, self::$instances['post'])) { |
| | | return self::$instances['post'][$id]; |
| | | } |
| | | $new = new self($id, 'post'); |
| | | self::$instances['post'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public static function forTerm(int $id): self |
| | | { |
| | | return new self($id, 'term'); |
| | | if (array_key_exists($id, self::$instances['term'])) { |
| | | return self::$instances['term'][$id]; |
| | | } |
| | | $new = new self($id, 'term'); |
| | | self::$instances['term'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public static function forUser(int $id): self |
| | | { |
| | | return new self($id, 'user'); |
| | | if (array_key_exists($id, self::$instances['user'])) { |
| | | return self::$instances['user'][$id]; |
| | | } |
| | | $new = new self($id, 'user'); |
| | | self::$instances['user'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for options |
| | | */ |
| | | public static function forOptions(?string $baseKey = null): self |
| | | public static function forOptions(?string $baseKey = 'ajv'): self |
| | | { |
| | | $instance = new self($baseKey, 'options'); |
| | | $instance->item->baseKey = $baseKey; |
| | | return $instance; |
| | | if (array_key_exists($baseKey, self::$instances['options'])) { |
| | | return self::$instances['options'][$baseKey]; |
| | | } |
| | | $new = new self($baseKey, 'options'); |
| | | self::$instances['options'][$baseKey] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple posts with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForPosts(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'post', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple terms with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForTerms(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'term', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple users with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForUsers(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'user', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Generic bulk loader |
| | | * @return array<int, Meta> |
| | | */ |
| | | protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array |
| | | { |
| | | if (empty($ids)) { |
| | | return []; |
| | | } |
| | | |
| | | $metas = []; |
| | | |
| | | // Create instances |
| | | foreach ($ids as $id) { |
| | | $metas[$id] = new self($id, $type); |
| | | } |
| | | |
| | | // Preload fields if specified |
| | | if (!empty($preloadFields)) { |
| | | self::bulkPreload($metas, $type, $preloadFields); |
| | | } |
| | | |
| | | return $metas; |
| | | } |
| | | |
| | | /** |
| | | * Bulk preload fields for multiple Meta instances |
| | | * @param Meta[] $metas |
| | | */ |
| | | protected static function bulkPreload(array $metas, string $objectType, array $fields): void |
| | | { |
| | | if (empty($metas) || empty($fields)) { |
| | | return; |
| | | } |
| | | |
| | | $ids = array_keys($metas); |
| | | $values = Storage::getBulkValues($ids, $objectType, $fields); |
| | | |
| | | // Distribute results to Meta instances |
| | | foreach ($values as $id => $fieldValues) { |
| | | if (!isset($metas[$id])) { |
| | | continue; |
| | | } |
| | | |
| | | $meta = $metas[$id]; |
| | | foreach ($fieldValues as $name => $value) { |
| | | $config = $meta->config($name) ?? ['type' => 'text']; |
| | | $field = new Field($name, $value, $config); |
| | | $meta->item()->setField($field); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Save multiple Meta instances efficiently |
| | | * @param Meta[] $metas |
| | | * @return array<int, bool> |
| | | */ |
| | | public static function saveBulk(array $metas, bool $updateTimestamp = true): array |
| | | { |
| | | // Validate all first |
| | | $invalid = []; |
| | | foreach ($metas as $id => $meta) { |
| | | if (!$meta->isValid()) { |
| | | $invalid[$id] = $meta->getErrors(); |
| | | } |
| | | } |
| | | |
| | | if (!empty($invalid)) { |
| | | JVB()->error()->log('meta', 'Bulk save has validation errors', [ |
| | | 'invalid_items' => $invalid |
| | | ], 'warning'); |
| | | } |
| | | |
| | | // Filter to only valid metas |
| | | $validMetas = array_filter($metas, fn($m) => $m->isValid()); |
| | | |
| | | // Check overrides before bulk save |
| | | foreach ($validMetas as $meta) { |
| | | foreach ($meta->item()->getDirtyFields() as $field) { |
| | | if ($meta->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $results = Storage::saveBulk($validMetas, $updateTimestamp); |
| | | |
| | | // Mark invalid ones as failed |
| | | foreach ($invalid as $id => $errors) { |
| | | $results[$id] = false; |
| | | } |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Constructor |
| | | // ───────────────────────────────────────────────────────────── |
| | | /*************************************************************** |
| | | * Constructor |
| | | ***************************************************************/ |
| | | |
| | | public function __construct(int|string|null $id, string $type) |
| | | { |
| | | $this->storage = new Storage(); |
| | | $this->validator = new Validator(); |
| | | $this->sanitizer = new Sanitizer(); |
| | | $this->typeManager = new MetaTypeManager(); |
| | | |
| | | $this->item = $this->buildItem($id, $type); |
| | | $this->type = $type; |
| | | $this->ID = $id; |
| | | $this->buildData($id, $type); |
| | | } |
| | | |
| | | protected function buildItem(int|string|null $id, string $type): Item |
| | | protected function buildData(int|string|null $id, string $type):void |
| | | { |
| | | $contentType = null; |
| | | $wpObject = null; |
| | | $this->wpObject = match($type) { |
| | | 'post' => get_post($id), |
| | | 'term' => get_term($id), |
| | | 'user', 'integrations' => get_userdata($id), |
| | | default => false |
| | | }; |
| | | |
| | | if ($id && $type !== 'options') { |
| | | [$wpObject, $contentType] = match ($type) { |
| | | 'post' => [get_post($id), jvbNoBase(get_post_type($id))], |
| | | 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)], |
| | | 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)], |
| | | default => [null, null] |
| | | }; |
| | | } |
| | | $this->slug = match($type) { |
| | | 'post' => $this->wpObject->post_type, |
| | | 'term' => $this->wpObject->taxonomy, |
| | | 'user' => jvbUserRole($id), |
| | | default => null |
| | | }; |
| | | |
| | | $item = new Item($id, $type, $contentType); |
| | | $item->wpObject = $wpObject; |
| | | $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type); |
| | | |
| | | // Mark WP defaults in configs |
| | | $defaults = Item::WP_DEFAULTS[$type] ?? []; |
| | | foreach ($defaults as $name) { |
| | | if (!isset($item->fieldConfigs[$name])) { |
| | | $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true]; |
| | | |
| | | $registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false; |
| | | $fields = $registrar ? $registrar->getFields() : []; |
| | | $meta = match($type) { |
| | | 'post' => get_post_meta($id), |
| | | 'term' => get_term_meta($id), |
| | | 'user' => get_user_meta($id), |
| | | default => [] |
| | | }; |
| | | $meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta); |
| | | |
| | | foreach ($fields as $fieldName => $config) { |
| | | $fieldName = jvbNoBase($fieldName); |
| | | if ($this->wpObject && property_exists($this->wpObject, $fieldName)) { |
| | | $config['wp'] = true; |
| | | $value = $this->wpObject->$fieldName; |
| | | } else if (in_array($fieldName, $this->defaults)) { |
| | | $config['wp'] = true; |
| | | switch ($fieldName) { |
| | | case 'post_thumbnail': |
| | | $value = get_post_thumbnail_id($this->ID); |
| | | break; |
| | | } |
| | | } else { |
| | | $item->fieldConfigs[$name]['_wp_default'] = true; |
| | | $value = array_key_exists(BASE.$fieldName, $meta) ? $meta[BASE.$fieldName] : $config['default']??''; |
| | | switch ($config['type']) { |
| | | case 'taxonomy': |
| | | if (!$config['isReference']??true) { |
| | | $config['wp'] = true; |
| | | $value = implode(',', wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids'])); |
| | | } |
| | | break; |
| | | case 'selector': |
| | | if (array_key_exists('subtype', $config) && $config['subtype'] === 'taxonomy' && !$config['isReference']??true) { |
| | | $config['wp'] = true; |
| | | $value = implode(',',wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids'])); |
| | | } |
| | | break; |
| | | } |
| | | |
| | | } |
| | | $this->fields[$fieldName] = new Field($fieldName, $value, $config); |
| | | } |
| | | |
| | | return $item; |
| | | } |
| | | |
| | | protected function loadFieldConfigs(?string $contentType, string $objectType): array |
| | | { |
| | | if (!$contentType && $objectType !== 'options') { |
| | | return []; |
| | | } |
| | | |
| | | return jvbGetFields($contentType ?? 'options', $objectType); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | |
| | | public function __isset(string $name): bool |
| | | { |
| | | return $this->item->hasField($name) || isset($this->computed[$name]); |
| | | return $this->item->hasField($name); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | return $this->getByPath($name); |
| | | } |
| | | |
| | | // Check computed fields first |
| | | if (isset($this->computed[$name])) { |
| | | return ($this->computed[$name])($this); |
| | | } |
| | | |
| | | // Return from loaded field if exists |
| | | if ($field = $this->item->getField($name)) { |
| | | return $field->get(); |
| | | } |
| | | |
| | | // Load from storage |
| | | $value = $this->storage->get($this->item, $name); |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | |
| | | $field = new Field($name, $value, $config); |
| | | $this->item->setField($field); |
| | | |
| | | return $value; |
| | | return $this->fields[$name]->get(); |
| | | } |
| | | |
| | | /** |
| | | * Set a field value (validates & sanitizes by default) |
| | | */ |
| | | public function set(string $name, mixed $value): self |
| | | public function set(string $name, mixed $value, $autosave = true): self |
| | | { |
| | | // Handle repeater subfield path (e.g., "services:2:image") |
| | | if (str_contains($name, ':')) { |
| | | return $this->setByPath($name, $value); |
| | | } |
| | | |
| | | $config = $this->item->getFieldConfig($name); |
| | | |
| | | if (!$config) { |
| | | // Allow setting unknown fields with minimal config |
| | | $config = ['type' => 'text', 'name' => $name]; |
| | | $field = $this->fields[$name]??false; |
| | | if (!$field) { |
| | | error_log('No config found for field: '.$name); |
| | | return $this; |
| | | } |
| | | |
| | | // Validate |
| | | if ($this->autoValidate && !$this->validator->validate($value, $config)) { |
| | | $field = $this->item->getField($name) ?? new Field($name, $value, $config); |
| | | if (!$this->validator->validate($value, $field->config)) { |
| | | $field->addError("Validation failed for {$name}"); |
| | | $this->item->setField($field); |
| | | return $this; |
| | | } |
| | | |
| | | // Sanitize |
| | | if ($this->autoSanitize) { |
| | | $value = $this->sanitizer->sanitize($value, $config); |
| | | } |
| | | |
| | | // Get or create field |
| | | $field = $this->item->getField($name); |
| | | $oldValue = null; |
| | | |
| | | if ($field) { |
| | | $oldValue = $field->value; |
| | | $field->set($value); |
| | | } else { |
| | | // Load original to track dirty state |
| | | $original = $this->storage->get($this->item, $name); |
| | | $oldValue = $original; |
| | | $field = new Field($name, $original, $config); |
| | | $field->set($value); |
| | | $this->item->setField($field); |
| | | } |
| | | |
| | | // Fire change callbacks |
| | | if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) { |
| | | foreach ($this->onChangeCallbacks[$name] as $callback) { |
| | | $callback($value, $oldValue, $this); |
| | | } |
| | | $value = $this->sanitizer->sanitize($value, $field->config); |
| | | $field->set($value); |
| | | if ($autosave && $field->isDirty) { |
| | | $this->save(); |
| | | } |
| | | |
| | | return $this; |
| | |
| | | public function getAll(array $fields = []): array |
| | | { |
| | | if (empty($fields) || $fields === ['all']) { |
| | | $fields = array_keys($this->item->fieldConfigs); |
| | | $fields = array_keys($this->fields); |
| | | } |
| | | $fields = array_filter($this->fields, function ($field) use ($fields) { |
| | | return in_array($field, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | // Load all from storage |
| | | $values = $this->storage->getAll($this->item, $fields); |
| | | |
| | | // Create Field instances |
| | | foreach ($values as $name => $value) { |
| | | if (!$this->item->getField($name)) { |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | $this->item->setField(new Field($name, $value, $config)); |
| | | } |
| | | } |
| | | |
| | | return $values; |
| | | return array_map(function ($field) { |
| | | return $field->value; |
| | | }, $fields); |
| | | } |
| | | |
| | | /** |
| | | * Set multiple fields |
| | | */ |
| | | public function setAll(array $data): self |
| | | public function setAll(array $data):bool |
| | | { |
| | | foreach ($data as $name => $value) { |
| | | $this->set($name, $value); |
| | | $this->set($name, $value, false); |
| | | } |
| | | return $this; |
| | | return $this->save(); |
| | | } |
| | | |
| | | /** |
| | | * Save all dirty fields to database |
| | | */ |
| | | public function save(bool $updateTimestamp = true): bool |
| | | public function save(): bool |
| | | { |
| | | if (!$this->item->isValid()) { |
| | | JVB()->error()->log('meta', 'Cannot save: validation errors exist', [ |
| | | 'fields' => array_keys($this->item->getInvalidFields()) |
| | | ], 'warning'); |
| | | return false; |
| | | } |
| | | $dirtyFields = array_filter($this->fields, function ($field) { |
| | | return $field->isDirty; |
| | | }); |
| | | $defaults = array_filter($dirtyFields, function ($field) { |
| | | return $field->isDefault; |
| | | }); |
| | | $custom = array_filter($dirtyFields, function($field) { |
| | | return !$field->isDefault; |
| | | }); |
| | | |
| | | // Check for field overrides before saving |
| | | foreach ($this->item->getDirtyFields() as $field) { |
| | | if ($this->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | $success = true; |
| | | |
| | | if (!empty($defaults)) { |
| | | $result = false; |
| | | switch ($this->type) { |
| | | case 'post': |
| | | //Deal with field exceptions, first, that cannot be set via wp_update_post |
| | | foreach ($defaults as $fieldName => $field) { |
| | | switch ($fieldName) { |
| | | case 'post_thumbnail': |
| | | $result = set_post_thumbnail($this->ID, $field->value); |
| | | unset($defaults[$fieldName]); |
| | | break; |
| | | } |
| | | //If it's a taxonomy, we use wp_set_object_terms |
| | | if ( |
| | | ($field->config['type'] === 'selector' && $field->config['subtype'] === 'taxonomy') |
| | | || $field->config['type'] === 'taxonomy') { |
| | | |
| | | $result = wp_set_object_terms($this->ID, array_map('absint', explode(',', $field->value)), jvbCheckBase($field->config['taxonomy'])); |
| | | unset($defaults[$fieldName]); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | if (!empty($defaults)) { |
| | | error_log('Remaining fields: '.print_r($defaults, true)); |
| | | $defaults = array_map(function ($field) { |
| | | return $field->value; |
| | | }, $defaults); |
| | | error_log('Remaining values to save: '.print_r($defaults, true)); |
| | | $data = array_merge([ |
| | | 'post_type' => $this->slug, |
| | | 'ID' => $this->ID |
| | | ], $defaults); |
| | | error_log('Updating post: '.print_r($data, true)); |
| | | $result = wp_update_post($data); |
| | | } |
| | | break; |
| | | case 'term': |
| | | $result = wp_update_term($this->ID, $this->slug, $defaults); |
| | | break; |
| | | case 'user': |
| | | $data = array_merge([ |
| | | 'ID' => $this->ID |
| | | ], $defaults); |
| | | $result = wp_update_user($data); |
| | | break; |
| | | } |
| | | if (!$result || is_wp_error($result)) { |
| | | $success = false; |
| | | } |
| | | } |
| | | if (!empty($custom)) { |
| | | $function = match ($this->type) { |
| | | 'post' => 'update_post_meta', |
| | | 'term' => 'update_term_meta', |
| | | 'user' => 'update_user_meta', |
| | | 'options', 'option' => 'update_option', |
| | | default => false, |
| | | }; |
| | | if (!$function) { |
| | | error_log('[Meta]::save() Invalid type, cannot save: '.$this->type); |
| | | return false; |
| | | } |
| | | foreach ($custom as $field) { |
| | | $result = $function($this->ID, BASE.$field->name, $field->value); |
| | | if (!$result) { |
| | | error_log('Problem saving field: '.$field->name.' with value: '.print_r($field->value, true)); |
| | | } |
| | | } |
| | | if ($this->type === 'term' && Registrar::getInstance($this->slug)->hasFeature('is_content')) { |
| | | update_term_meta($this->ID, BASE.'date_modified', date('Y-m-d H:i:s')); |
| | | } |
| | | } |
| | | |
| | | return $this->storage->save($this->item, $updateTimestamp); |
| | | |
| | | return $success; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function delete(string $name): bool |
| | | { |
| | | $result = $this->storage->delete($this->item, $name); |
| | | |
| | | if ($result && $field = $this->item->getField($name)) { |
| | | $field->set($this->getDefaultValue($name)); |
| | | $field->markClean(); |
| | | $field = $this->fields[$name]??false; |
| | | if (!$field) { |
| | | error_log('[Meta]::delete Could not delete field '.$name.': not registered'); |
| | | return true; |
| | | } |
| | | |
| | | return $result; |
| | | if ($field->isTaxonomy()) { |
| | | wp_set_object_terms($this->ID, [], $this->slug); |
| | | return true; |
| | | } |
| | | |
| | | return match ($this->type) { |
| | | 'post' => delete_post_meta($this->ID, BASE.$name), |
| | | 'term' => delete_term_meta($this->ID, BASE.$name), |
| | | 'user', 'integrations' => delete_user_meta($this->ID, BASE.$name), |
| | | 'options' => delete_option(BASE.$name), |
| | | default => false |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Delete multiple field values |
| | | */ |
| | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Repeater Access |
| | | // ───────────────────────────────────────────────────────────── |
| | | /***************************************************************** |
| | | * Repeater Access |
| | | *****************************************************************/ |
| | | |
| | | /** |
| | | * Get repeater accessor for fluent repeater operations |
| | |
| | | |
| | | protected function setByPath(string $path, mixed $value): self |
| | | { |
| | | error_log('Setting by path: '.$path.', with value: '.print_r($value, true)); |
| | | $parts = explode(':', $path, 3); |
| | | if (count($parts) !== 3) { |
| | | return $this; |
| | |
| | | // Utility Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get all dirty (changed) field values |
| | | */ |
| | | public function getDirty(): array |
| | | { |
| | | return array_map( |
| | | fn(Field $f) => $f->value, |
| | | $this->item->getDirtyFields() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Check if any fields have changed |
| | | */ |
| | | public function isDirty(): bool |
| | | { |
| | | return $this->item->hasDirtyFields(); |
| | | } |
| | | |
| | | /** |
| | | * Discard all unsaved changes |
| | |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get validation errors |
| | | */ |
| | | public function getErrors(): array |
| | | { |
| | | $errors = []; |
| | | foreach ($this->item->getInvalidFields() as $name => $field) { |
| | | $errors[$name] = $field->errors; |
| | | } |
| | | return $errors; |
| | | } |
| | | |
| | | /** |
| | | * Check if valid (no validation errors) |
| | | */ |
| | | public function isValid(): bool |
| | | { |
| | | return $this->item->isValid(); |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-validation for bulk operations |
| | | */ |
| | | public function withoutValidation(): self |
| | | { |
| | | $this->autoValidate = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-sanitization |
| | | */ |
| | | public function withoutSanitization(): self |
| | | { |
| | | $this->autoSanitize = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Re-enable validation and sanitization |
| | | */ |
| | | public function withDefaults(): self |
| | | { |
| | | $this->autoValidate = true; |
| | | $this->autoSanitize = true; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get the underlying Item |
| | | */ |
| | | public function item(): Item |
| | | { |
| | | return $this->item; |
| | | } |
| | | |
| | | /** |
| | | * Get field configuration |
| | | */ |
| | | public function config(string $name): ?array |
| | | { |
| | | return $this->item->getFieldConfig($name); |
| | | return $this->fields[$name]->config??null; |
| | | } |
| | | |
| | | /** |
| | | * Get all field configurations |
| | | */ |
| | | public function configs(): array |
| | | { |
| | | return $this->item->fieldConfigs; |
| | | } |
| | | |
| | | /** |
| | | * Get item ID |
| | | */ |
| | | public function id(): int|string|null |
| | | { |
| | | return $this->item->id; |
| | | return $this->ID; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function objectType(): string |
| | | { |
| | | return $this->item->objectType; |
| | | return $this->type; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function contentType(): ?string |
| | | { |
| | | return $this->item->contentType; |
| | | return $this->contentType; |
| | | } |
| | | |
| | | /** |
| | | * Eager load all fields |
| | | */ |
| | | public function eager(): self |
| | | { |
| | | $this->getAll(); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Convert loaded fields to array |
| | | */ |
| | | public function toArray(): array |
| | | { |
| | | return $this->item->toArray(); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Event Callbacks |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Register callback for field changes |
| | | */ |
| | | public function onChange(string $field, callable $callback): self |
| | | { |
| | | $this->onChangeCallbacks[$field][] = $callback; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Register computed/virtual field |
| | | */ |
| | | public function computed(string $name, callable $getter): self |
| | | { |
| | | $this->computed[$name] = $getter; |
| | | return $this; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Protected Helpers |