| | |
| | | <?php |
| | | namespace JVBase\meta; |
| | | |
| | | use Exception; |
| | | |
| | | 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 |
| | | { |
| | | protected Item $item; |
| | | protected Storage $storage; |
| | | protected MetaValidator $validator; |
| | | protected MetaSanitizer $sanitizer; |
| | | protected Validator $validator; |
| | | protected Sanitizer $sanitizer; |
| | | protected MetaTypeManager $typeManager; |
| | | |
| | | protected bool $autoValidate = true; |
| | | protected bool $autoSanitize = true; |
| | | |
| | | /** @var array<string, callable[]> */ |
| | | protected array $onChangeCallbacks = []; |
| | | |
| | | /** @var array<string, callable> */ |
| | | protected array $computed = []; |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Factory Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Create Meta instance for a post |
| | | */ |
| | | public static function forPost(int $id): self |
| | | { |
| | | return new self($id, 'post'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for a term |
| | | */ |
| | | public static function forTerm(int $id): self |
| | | { |
| | | return new self($id, 'term'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for a user |
| | | */ |
| | | public static function forUser(int $id): self |
| | | { |
| | | return new self($id, 'user'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for options |
| | | */ |
| | | public static function forOptions(?string $baseKey = null): self |
| | | { |
| | | $instance = new self($baseKey, 'options'); |
| | |
| | | return $instance; |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | public function __construct(int|string|null $id, string $type) |
| | | { |
| | | $this->storage = new Storage(); |
| | | $this->validator = new MetaValidator(); |
| | | $this->sanitizer = new MetaSanitizer(); |
| | | $this->validator = new Validator(); |
| | | $this->sanitizer = new Sanitizer(); |
| | | $this->typeManager = new MetaTypeManager(); |
| | | |
| | | $this->item = $this->buildItem($id, $type); |
| | |
| | | |
| | | public function __isset(string $name): bool |
| | | { |
| | | return $this->item->hasField($name); |
| | | return $this->item->hasField($name) || isset($this->computed[$name]); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | */ |
| | | public function get(string $name): mixed |
| | | { |
| | | // 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(); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Set a field value (validates & sanitizes) |
| | | * Set a field value (validates & sanitizes by default) |
| | | */ |
| | | public function set(string $name, mixed $value): self |
| | | { |
| | |
| | | $config = ['type' => 'text', 'name' => $name]; |
| | | } |
| | | |
| | | $config['name'] = $name; |
| | | |
| | | // Validate |
| | | if ($this->autoValidate && !$this->validator->validate($value, $config)) { |
| | | $field = $this->item->getField($name) ?? new Field($name, $value, $config); |
| | |
| | | |
| | | // Get or create field |
| | | $field = $this->item->getField($name); |
| | | $oldValue = null; |
| | | |
| | | if ($field) { |
| | | $oldValue = $field->value; |
| | | $field->set($value); |
| | | } else { |
| | | // Need to load original to track dirty state |
| | | // 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); |
| | | } |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | |
| | |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | | * Delete multiple field values |
| | | */ |
| | | public function deleteAll(array $names): array |
| | | { |
| | | $results = []; |
| | | foreach ($names as $name) { |
| | | $results[$name] = $this->delete($name); |
| | | } |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Repeater Access |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get repeater accessor for fluent repeater operations |
| | | */ |
| | | public function repeater(string $name): Repeater |
| | | { |
| | | return new Repeater($this, $name); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Utility Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get all dirty (changed) fields |
| | | * Get all dirty (changed) field values |
| | | */ |
| | | public function getDirty(): array |
| | | { |
| | |
| | | */ |
| | | public function reset(): self |
| | | { |
| | | foreach ($this->item->fields as $field) { |
| | | $field->reset(); |
| | | } |
| | | $this->item->resetAll(); |
| | | return $this; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * Get the underlying item (for rendering, etc) |
| | | * 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->fieldConfigs; |
| | | } |
| | | |
| | | /** |
| | | * Get item ID |
| | | */ |
| | | public function id(): int|string|null |
| | | { |
| | | return $this->item->id; |
| | | } |
| | | |
| | | /** |
| | | * Get object type (post, term, user, options) |
| | | */ |
| | | public function objectType(): string |
| | | { |
| | | return $this->item->objectType; |
| | | } |
| | | |
| | | /** |
| | | * Get content type (tattoo, artist, etc) |
| | | */ |
| | | public function contentType(): ?string |
| | | { |
| | | return $this->item->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 |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | protected function checkOverrides(Field $field): bool |
| | | /** |
| | | * Check for field update overrides |
| | | */ |
| | | public function checkOverrides(Field $field): bool |
| | | { |
| | | $name = $field->name; |
| | | $type = $field->type(); |
| | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Get default value for a field type |
| | | */ |
| | | protected function getDefaultValue(string $name): mixed |
| | | { |
| | | $config = $this->item->getFieldConfig($name); |
| | |
| | | default => '', |
| | | }; |
| | | } |
| | | |
| | | } |