$layouts * @param list $useOn * @param list $excludeFrom */ public function __construct( public readonly int $id, public readonly string $title, public readonly string $status, public readonly bool $default, public readonly bool $enabled, public readonly array $layouts, public readonly array $useOn, public readonly array $excludeFrom, public readonly bool $attached, public readonly ?string $markedUnused ) { } /** @param array $divi et_theme_builder_get_template() */ public static function fromDivi(array $divi, string $status, bool $attached, ?string $markedUnused): self { $layouts = []; foreach (self::AREAS as $area) { $layout = is_array($divi['layouts'][$area] ?? null) ? $divi['layouts'][$area] : []; $layouts[$area] = ['id' => (int) ($layout['id'] ?? 0), 'enabled' => (bool) ($layout['enabled'] ?? true)]; } return new self( (int) ($divi['id'] ?? 0), (string) ($divi['title'] ?? ''), $status, (bool) ($divi['default'] ?? false), (bool) ($divi['enabled'] ?? false), $layouts, self::strings($divi['use_on'] ?? []), self::strings($divi['exclude_from'] ?? []), $attached, $markedUnused ); } /** * Whether visitors can see it: listed by the live Theme Builder post and enabled. Divi renders such a * template whatever its post status (et_theme_builder_get_template() and ThemeBuilderRequest only read * the enabled flag), so the status does not count here – fail closed (ruling Q8/B8). */ public function isLive(): bool { return $this->attached && $this->enabled; } /** Whether visitors see layout $layoutId as its $area through this template. */ public function showsLayout(string $area, int $layoutId): bool { return $this->isLive() && ($this->layouts[$area]['id'] ?? 0) === $layoutId && ($this->layouts[$area]['enabled'] ?? false); } /** template_rev: changes with everything the Theme Builder tools read or write. */ public function rev(): string { $state = [$this->id, $this->title, $this->enabled, $this->layouts]; // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure value object, no WordPress. return Rev::of((string) json_encode( array_merge($state, [$this->useOn, $this->excludeFrom, $this->attached]), JSON_UNESCAPED_UNICODE )); } /** @return list Post types of its singular conditions (singular:post_type::…). */ public function postTypes(): array { return self::postTypesOf($this->useOn); } /** * @param list $useOn * @return list */ public static function postTypesOf(array $useOn): array { $types = []; foreach ($useOn as $condition) { $parts = explode(':', $condition); if (($parts[0] ?? '') === 'singular' && ($parts[1] ?? '') === 'post_type' && ($parts[2] ?? '') !== '') { $types[] = $parts[2]; } } return array_values(array_unique($types)); } /** @return array */ public function toArray(): array { return [ 'id' => $this->id, 'title' => $this->title, 'status' => $this->status, 'default' => $this->default, 'enabled' => $this->enabled, 'live' => $this->isLive(), 'attached' => $this->attached, 'marked_unused' => $this->markedUnused, 'template_rev' => $this->rev(), 'layouts' => $this->layouts, 'use_on' => $this->useOn, 'exclude_from' => $this->excludeFrom, ]; } /** * Live templates other than $exceptId that share a condition of $useOn (Divi then picks one of them). * * @param list