docs: add url transformer implementation plan #1
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WPContentSync\Url;
|
||||||
|
|
||||||
|
final class MetadataUrlTransformer {
|
||||||
|
private UrlTransformer $url_transformer;
|
||||||
|
|
||||||
|
public function __construct( UrlTransformer $url_transformer ) {
|
||||||
|
$this->url_transformer = $url_transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform URLs within a metadata value.
|
||||||
|
*
|
||||||
|
* @param mixed $value Metadata value.
|
||||||
|
* @param UrlMappingCollection $mappings URL mappings.
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function transformValue( $value, UrlMappingCollection $mappings ) {
|
||||||
|
if ( is_array( $value ) ) {
|
||||||
|
return $this->transformArray( $value, $mappings );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_string( $value ) ) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->isSerialized( $value ) ) {
|
||||||
|
// phpcs:ignore -- Required to transform serialized metadata while preserving valid string lengths.
|
||||||
|
$unserialized = unserialize( $value, array( 'allowed_classes' => false ) );
|
||||||
|
|
||||||
|
// phpcs:ignore -- Required to reserialize metadata with updated string lengths.
|
||||||
|
return serialize( $this->transformValue( $unserialized, $mappings ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->isJsonObjectOrArray( $value ) ) {
|
||||||
|
$decoded = json_decode( $value, true );
|
||||||
|
|
||||||
|
if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) {
|
||||||
|
$encoded = wp_json_encode( $this->transformArray( $decoded, $mappings ) );
|
||||||
|
|
||||||
|
if ( is_string( $encoded ) ) {
|
||||||
|
return $encoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->url_transformer->transformString( $value, $mappings );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform URLs within a metadata array.
|
||||||
|
*
|
||||||
|
* @param array<mixed> $value Metadata array.
|
||||||
|
* @param UrlMappingCollection $mappings URL mappings.
|
||||||
|
* @return array<mixed>
|
||||||
|
*/
|
||||||
|
private function transformArray( array $value, UrlMappingCollection $mappings ): array {
|
||||||
|
$transformed = array();
|
||||||
|
|
||||||
|
foreach ( $value as $key => $item ) {
|
||||||
|
$transformed[ $key ] = $this->transformValue( $item, $mappings );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSerialized( string $value ): bool {
|
||||||
|
$trimmed = trim( $value );
|
||||||
|
|
||||||
|
if ( 'N;' === $trimmed ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'b:0;' === $trimmed ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! preg_match( '/^(?:a|O|s|i|d|b):/', $trimmed ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore -- Validation must parse PHP serialized metadata.
|
||||||
|
return false !== @unserialize( $trimmed, array( 'allowed_classes' => false ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isJsonObjectOrArray( string $value ): bool {
|
||||||
|
$trimmed = trim( $value );
|
||||||
|
|
||||||
|
return '' !== $trimmed && in_array( $trimmed[0], array( '{', '[' ), true );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WPContentSync\Tests\Unit\Url;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use WPContentSync\Url\MetadataUrlTransformer;
|
||||||
|
use WPContentSync\Url\UrlMapping;
|
||||||
|
use WPContentSync\Url\UrlMappingCollection;
|
||||||
|
use WPContentSync\Url\UrlTransformer;
|
||||||
|
|
||||||
|
class MetadataUrlTransformerTest extends TestCase {
|
||||||
|
private function mappings(): UrlMappingCollection {
|
||||||
|
return new UrlMappingCollection(
|
||||||
|
array(
|
||||||
|
new UrlMapping( 'https://example.test', 'https://staging.example.test' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_recursively_transforms_array_metadata(): void {
|
||||||
|
$transformer = new MetadataUrlTransformer( new UrlTransformer() );
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
array(
|
||||||
|
'hero' => array(
|
||||||
|
'url' => 'https://staging.example.test/uploads/hero.jpg',
|
||||||
|
),
|
||||||
|
'count' => 3,
|
||||||
|
'flag' => true,
|
||||||
|
),
|
||||||
|
$transformer->transformValue(
|
||||||
|
array(
|
||||||
|
'hero' => array(
|
||||||
|
'url' => 'https://example.test/uploads/hero.jpg',
|
||||||
|
),
|
||||||
|
'count' => 3,
|
||||||
|
'flag' => true,
|
||||||
|
),
|
||||||
|
$this->mappings()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_transforms_json_strings(): void {
|
||||||
|
$transformer = new MetadataUrlTransformer( new UrlTransformer() );
|
||||||
|
$result = $transformer->transformValue(
|
||||||
|
'{"url":"https:\/\/example.test\/uploads\/hero.jpg"}',
|
||||||
|
$this->mappings()
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame( '{"url":"https:\/\/staging.example.test\/uploads\/hero.jpg"}', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_preserves_serialized_data_validity(): void {
|
||||||
|
$transformer = new MetadataUrlTransformer( new UrlTransformer() );
|
||||||
|
// phpcs:ignore -- Test fixture requires native serialized metadata.
|
||||||
|
$serialized = serialize(
|
||||||
|
array(
|
||||||
|
'url' => 'https://example.test/uploads/hero.jpg',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $transformer->transformValue( $serialized, $this->mappings() );
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
array(
|
||||||
|
'url' => 'https://staging.example.test/uploads/hero.jpg',
|
||||||
|
),
|
||||||
|
// phpcs:ignore -- Assertion verifies the transformed serialized metadata remains valid.
|
||||||
|
unserialize( $result )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_transforms_plain_string_metadata(): void {
|
||||||
|
$transformer = new MetadataUrlTransformer( new UrlTransformer() );
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
'https://staging.example.test/contact',
|
||||||
|
$transformer->transformValue( 'https://example.test/contact', $this->mappings() )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,19 @@ if ( ! function_exists( 'wp_parse_url' ) ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_json_encode' ) ) {
|
||||||
|
/**
|
||||||
|
* Minimal JSON encoder for unit tests.
|
||||||
|
*
|
||||||
|
* @param mixed $value Value to encode.
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
function wp_json_encode( $value ) {
|
||||||
|
// phpcs:ignore -- Test stub for WordPress' wp_json_encode().
|
||||||
|
return json_encode( $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! function_exists( 'get_option' ) ) {
|
if ( ! function_exists( 'get_option' ) ) {
|
||||||
/**
|
/**
|
||||||
* Minimal WordPress option reader for unit tests.
|
* Minimal WordPress option reader for unit tests.
|
||||||
|
|||||||
Reference in New Issue
Block a user