Синхронизация стилей сайта с theme.json

Поговорим как программно обновить раздел "styles" в theme.json взяв их из текущих настроект сайта. В (FSE теме) WordPress основные настройки дизайна хранятся в файле theme.json.

При этом часть этих параметров можно изменить через редактор сайта WordPress, через раздел «Стили».

Проблема в том, что изменения из редактора не записываются обратно в файл темы theme.json. WordPress сохраняет их в базе данных.

Из-за этого итоговый дизайн темы состоит из двух частей:

  • базовые стили из theme.json;
  • пользовательские изменения из базы данных.

Иногда требуется перенести настройки, сделанные через редактор, обратно в файл темы. Например, чтобы сохранить их в Git, перенести на другой сайт или сделать частью новой версии темы.

Что делает код

Класс получает пользовательские стили, объединяет их со стилями из текущего theme.json и записывает результат обратно в файл.

При этом заменяется только раздел styles.

Остальные части theme.json, например settings, остаются без изменений. Их исходное форматирование также сохраняется.

Код файла: Theme_Json_Styles_Updater.php

GitHub
<?php

/**
 * Updates the root `styles` section in the theme.json file.
 *
 * The class retrieves user styles saved through the Site Editor,
 * merges them with the styles defined in theme.json, and replaces only
 * the `styles` section. Formatting of the other sections remains unchanged.
 *
 * Usage:
 *
 *     $updater = new Theme_Json_Styles_Updater(
 *         theme_json_file: get_stylesheet_directory() . '/theme.json',
 *         indent: '  '
 *     );
 *
 *     $updater->update();
 *
 * Version: 1.0
 */
final class Theme_Json_Styles_Updater {

	public function __construct(
		private readonly string $theme_json_file,
		private readonly string $indent = '  '
	) {}

	public function update(): void {
		$theme_json = $this->read_theme_json();
		$styles     = $this->get_merged_styles( $theme_json );
		$new_styles = $this->encode_styles( $styles );
		$theme_json = $this->replace_root_object( $theme_json, 'styles', $new_styles );

		$this->write_theme_json( $theme_json );
	}

	private function read_theme_json(): string {
		$contents = file_get_contents( $this->theme_json_file );

		if ( false === $contents ) {
			throw new RuntimeException( 'Unable to read theme.json.' );
		}

		return $contents;
	}

	private function get_merged_styles( string $theme_json ): array {
		$theme_data = json_decode(
			$theme_json,
			true,
			512,
			JSON_THROW_ON_ERROR
		);

		$user_data = WP_Theme_JSON_Resolver::get_user_data()->get_raw_data();

		return array_replace_recursive(
			$theme_data['styles'] ?? [],
			$user_data['styles'] ?? []
		);
	}

	private function encode_styles( array $styles ): string {
		$json = wp_json_encode(
			$styles,
			JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
		);

		if ( ! $json ) {
			throw new RuntimeException( 'Unable to encode styles.' );
		}

		$json = preg_replace_callback(
			'/^( +)/m',
			fn( $match ) => str_repeat( $this->indent, strlen( $match[1] ) / 4 ),
			$json
		);

		return $this->indent_json( $json );
	}

	private function indent_json( string $json ): string {
		return preg_replace( '/^/m', $this->indent, $json ) ?: $json;
	}

	private function replace_root_object( string $json, string $property, string $replacement ): string {
		$start = $this->find_root_object_start( $json, $property );
		$end   = $this->find_object_end( $json, $start );

		return substr_replace(
			$json,
			ltrim( $replacement ),
			$start,
			$end - $start
		);
	}

	private function find_root_object_start( string $json, string $property ): int {
		$property_position = strpos( $json, "\"$property\"" );
		if ( false === $property_position ) {
			throw new RuntimeException( "Property \"$property\" was not found." );
		}

		$object_start = strpos( $json, '{', $property_position );
		if ( false === $object_start ) {
			throw new RuntimeException( "Object \"$property\" was not found." );
		}

		return $object_start;
	}

	private function find_object_end( string $json, int $object_start ): int {
		$depth     = 0;
		$in_string = false;
		$escaped   = false;
		$length    = strlen( $json );

		for ( $position = $object_start; $position < $length; $position++ ) {
			$character = $json[ $position ];

			if ( $in_string ) {
				if ( $escaped ) {
					$escaped = false;
				} elseif ( '\\' === $character ) {
					$escaped = true;
				} elseif ( '"' === $character ) {
					$in_string = false;
				}

				continue;
			}

			if ( '"' === $character ) {
				$in_string = true;
			} elseif ( '{' === $character ) {
				$depth++;
			} elseif ( '}' === $character && 0 === --$depth ) {
				return $position + 1;
			}
		}

		throw new RuntimeException( 'Unable to find the end of the object.' );
	}

	private function write_theme_json( string $theme_json ): void {
		if ( ! file_put_contents( $this->theme_json_file, $theme_json ) ) {
			throw new RuntimeException( 'Unable to write theme.json.' );
		}
	}

}

Код файла: usage.php

GitHub
<?php

$updater = new Theme_Json_Styles_Updater(
	theme_json_file: get_stylesheet_directory() . '/theme.json',
	indent: '  '
);

$updater->update();

Теперь запускаем код

Создаём объект класса, передаём путь к theme.json и вызываем метод update():

$updater = new Theme_Json_Styles_Updater(
	get_stylesheet_directory() . '/theme.json',
	'  '
);

$updater->update();

Второй аргумент задаёт отступ:

  • ' ' - два пробела;
  • "\t" - табуляция.

Заметки

Как работает обновление

Сначала класс читает текущее содержимое файла theme.json.

Затем класс получает пользовательские настройки через:

WP_Theme_JSON_Resolver::get_user_data()

Этот метод возвращает данные, сохранённые WordPress после изменений в редакторе сайта.

Из полученных данных используется только раздел styles.

Стили из базы объединяются со стилями из файла. При совпадении значение из базы данных приоритетнее.

Например, в файле указано:

{
  "styles": {
	"color": {
	  "text": "#111111",
	  "background": "#ffffff"
	}
  }
}

А через редактор сайта пользователь изменил только цвет текста:

{
  "styles": {
	"color": {
	  "text": "#333333"
	}
  }
}

После объединения получится:

{
  "styles": {
	"color": {
	  "text": "#333333",
	  "background": "#ffffff"
	}
  }
}

Почему не перезаписывается весь theme.json

Самый простой вариант выглядел бы так:

  1. Прочитать весь файл.
  2. Преобразовать его через json_decode().
  3. Изменить раздел styles.
  4. Сохранить весь файл через json_encode().

Но в этом случае PHP полностью изменит форматирование файла.

Например, компактная запись:

"margin": { "top": "20px", "bottom": "20px" }

может превратиться в:

"margin": {
  "top": "20px",
  "bottom": "20px"
}

Также могут измениться отступы и расположение переносов строк во всём файле.

Поэтому класс находит начало и конец только корневого объекта styles, а затем заменяет этот фрагмент в исходной строке.

Так форматирование остальных разделов остаётся нетронутым.

Важный момент

После переноса настроек пользовательские стили останутся в базе данных.

Это значит, что WordPress продолжит применять их поверх theme.json. Обычно результат останется тем же, потому что в файл были записаны такие же значения.

Но если затем вручную изменить эти параметры в theme.json, сохранённые настройки из базы могут продолжить их перекрывать.

Поэтому после успешного переноса может потребоваться сбросить пользовательские стили в редакторе сайта или удалить соответствующую запись wp_global_styles.