From d2535396acbd28340e268054fdc68f9dacf3da07 Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Thu, 29 May 2025 13:41:08 +0200 Subject: [PATCH 01/12] fix: Tweaks and features --- src/Core/XWC_Data.php | 32 ++++ src/Core/XWC_Prop.php | 227 +++++++++++++++++++++++++ src/Decorators/Model.php | 8 +- src/Decorators/Model_Modifier.php | 4 +- src/Entity.php | 4 +- src/Interfaces/XWC_Data_Definition.php | 2 +- src/Model/Prop_Getters.php | 28 ++- src/Model/Prop_Setters.php | 25 ++- 8 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 src/Core/XWC_Prop.php diff --git a/src/Core/XWC_Data.php b/src/Core/XWC_Data.php index f67c073..88f65bb 100644 --- a/src/Core/XWC_Data.php +++ b/src/Core/XWC_Data.php @@ -91,6 +91,14 @@ public function __call( string $name, array $args ): mixed { : $this->set_prop( $prop, $args[0] ); } + public function jsonSerialize(): mixed { + $data = $this->get_data(); + + unset( $data['meta_data'], $data['stats'] ); + + return $data; + } + /** * Load the data for this object from the database. * @@ -133,6 +141,7 @@ public function save() { : array( 'created', null ); return $this + ->maybe_set_object() ->maybe_set_date( ...$args ) ->save_wc_data(); } @@ -328,6 +337,29 @@ protected function is_internal_meta_key( $key ) { return $parent_check; } + protected function maybe_set_object(): static { + if ( ! in_array( 'object', $this->get_prop_types(), true ) ) { + return $this; + } + + $changed = array_diff( + (array) $this->get_prop_by_type( 'object' ), + array_keys( $this->get_changes() ), + ); + + foreach ( $changed as $prop ) { + $obj = $this->get_prop( $prop ); + + if ( ! $obj?->changed() ) { + continue; + } + + $this->set_prop( $prop, $obj ); + } + + return $this; + } + /** * Maybe set the created or updated date on save. * diff --git a/src/Core/XWC_Prop.php b/src/Core/XWC_Prop.php new file mode 100644 index 0000000..7b358c2 --- /dev/null +++ b/src/Core/XWC_Prop.php @@ -0,0 +1,227 @@ + + */ +class XWC_Prop implements ArrayAccess, JsonSerializable { + /** + * Traversible data array. + * + * @var array + */ + protected array $data = array(); + + /** + * The hash of the data. + * + * This is used to track changes to the data. + * + * @var string + */ + protected string $hash = ''; + + /** + * Gets the default JSON representation of the object. + * + * @return static + */ + public static function default(): static { + return new static(); + } + + /** + * Creates a new instance from JSON data. + * + * @template Tp of XWC_Prop + * @param null|false|array{class?: class-string, data?: array, hash?: string} $data The JSON data. + * @return Tp + */ + public static function from_json( null|bool|array $data ): XWC_Prop { + if ( ! is_array( $data ) || ! $data ) { + $data = array( + 'data' => array(), + 'hash' => '', + ); + } + $data['class'] ??= static::class; + + $cname = is_a( $data['class'], static::class, true ) ? $data['class'] : static::class; + + return new $data['class']( $data ); + } + + /** + * Constructor. + * + * @param array{data?: array, hash?: string} $args Data and hash. + * } + */ + public function __construct( array $args = array() ) { + if ( isset( $args['data'] ) ) { + $this->sort( $args['data'] ); + } + + $this->data = $args['data'] ?? array(); + $this->hash = $args['hash'] ?? $this->hash_data(); + } + + /** + * Serializes the object to an array. + * + * @return array{data: array, hash: string} Data and hash. + */ + public function __serialize(): array { + return array( + 'data' => $this->data, + 'hash' => $this->hash_data(), + ); + } + + /** + * Unserializes the object from an array. + * + * @param array{data?: array, hash?: string} $data Data and hash. + */ + public function __unserialize( array $data ): void { + $this->data = $data['data'] ?? array(); + $this->hash = $data['hash'] ?? $this->hash_data(); + } + + /** + * Return data needed for JSON serialization. + * + * @return array{ + * class:class-string, + * data: array, + * hash: string, + * } + */ + public function jsonSerialize(): mixed { + return array( + 'class' => static::class, + 'data' => $this->get_data(), + 'hash' => $this->hash_data(), + ); + } + + /** + * Sets the value at the specified offset. + * + * @param TKey $offset The offset to set. + * @param TValue $value The value to set. + * @return static + */ + public function set( string $offset, mixed $value ): static { + $this->data[ $offset ] = $value; + + return $this; + } + + /** + * Loads data into the property. + * + * @param array $data The data to load. + * @return static + */ + public function set_data( array $data ): static { + $this->sort( $data ); + + $this->data = $data; + + return $this; + } + + /** + * Gets the value at the specified offset. + * + * @param TKey $offset The offset to retrieve. + * @return TValue|null Can return any type or null if not set. + */ + public function get( string $offset ): mixed { + return $this->data[ $offset ] ?? null; + } + + public function get_data(): array { + return $this->data; + } + + public function changed(): bool { + return array() !== $this->data && $this->hash !== $this->hash_data(); + } + + /** + * Assigns a value to the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to assign the value to. + * @param TValue $value The value to set. + * @return void + */ + public function offsetSet( $offset, $value ): void { + throw new \BadMethodCallException( 'Do not use this method directly. Use the setter method instead.' ); + } + + /** + * Returns the value at the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to retrieve. + * @return TValue|array Can return any type. + */ + public function &offsetGet( $offset ): mixed { + return $this->data[ $offset ] ?? array(); + } + + /** + * Checks if the specified offset exists. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to check. + * @return bool + */ + public function offsetExists( $offset ): bool { + return isset( $this->data[ $offset ] ); + } + + /** + * Unsets the value at the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to unset. + * @return void + */ + public function offsetUnset( $offset ): void { + throw new \BadMethodCallException( 'Do not use this method directly. Use the setter method instead.' ); + } + + private function hash_data(): string { + $data = $this->data; + + ksort( $data ); + + return hash( 'md5', (string) wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ); + } + + private function sort( array &$arr ): void { + array_is_list( $arr ) + ? sort( $arr, SORT_NATURAL | SORT_FLAG_CASE ) + : ksort( $arr, SORT_NATURAL | SORT_FLAG_CASE ); + + foreach ( $arr as &$value ) { + if ( ! is_array( $value ) ) { + continue; + } + + $this->sort( $value ); + } + } +} diff --git a/src/Decorators/Model.php b/src/Decorators/Model.php index 655e4fb..cb66a6a 100644 --- a/src/Decorators/Model.php +++ b/src/Decorators/Model.php @@ -47,7 +47,7 @@ class Model { * * @var array $core_props Array of core properties. * @param array|null $data_store Data store class name. * @param array * } | array{0: 'enum', 1: array{0: class-string}} */ @@ -148,7 +150,7 @@ protected function get_prop_type( string $prop ): array { /** * Variable narrowing for prop types. * - * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'string'|'other' $type + * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string $type */ $type = \array_shift( $types ); @@ -222,6 +224,7 @@ protected function get_prop( $prop, $context = 'view' ) { 'json_obj' => $this->get_json_prop( $value, \JSON_FORCE_OBJECT ), 'binary' => $this->get_binary_prop( $value ), 'base64_string' => $this->get_base64_string_prop( $value ), + 'object' => $this->get_object_prop( $value, $prop ), default => $this->get_unknown_prop( $type, $prop, $value ), }; } @@ -319,6 +322,27 @@ protected function get_base64_string_prop( ?string $value ): string { return ! $this->is_base64_string( $value ) ? \base64_encode( $value ) : $value; } + protected function get_object_prop( mixed $value, string $prop ): string { + $iof = static fn( $t ) => $t instanceof JsonSerializable || $t instanceof Stringable; + + if ( $iof( $value ) ) { + $value = $this->get_json_prop( $value, \JSON_FORCE_OBJECT | \JSON_UNESCAPED_UNICODE ); + } + + if ( \is_string( $value ) && \class_exists( $value ) ) { + $value = ''; + } + + $value = $value ?: \wp_json_encode( + array( + 'class' => $this->default_data[ $prop ], + 'data' => array(), + ), + ); + + return $value; + } + protected function get_unknown_prop( string $type, string $prop, mixed $value ): mixed { if ( \method_exists( $this, "get_{$type}_prop" ) ) { $value = $this->{"get_{$type}_prop"}( $value, $prop ); diff --git a/src/Model/Prop_Setters.php b/src/Model/Prop_Setters.php index 4c99a77..03a9434 100644 --- a/src/Model/Prop_Setters.php +++ b/src/Model/Prop_Setters.php @@ -3,9 +3,11 @@ namespace XWC\Data\Model; use BackedEnum; +use WC_Data_Exception; use XWC_Data; use XWC_Data_Store_XT; use XWC_Meta_Store; +use XWC_Prop; /** * Prop setters trait. @@ -22,7 +24,7 @@ trait Prop_Setters { * * @param string $prop Name of prop to get type for. * @return array{ - * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'string'|'other', + * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, * 1: array * } | array{0: 'enum', 1: array{0: class-string}} */ @@ -111,6 +113,7 @@ protected function set_prop( $prop, $value ): static { 'float' => $this->set_float_prop( $prop, $value ), 'slug' => $this->set_slug_prop( $prop, $value ), 'string' => $this->set_wc_data_prop( $prop, $value ), + 'object' => $this->set_object_prop( $prop, $value ), default => $this->set_unknown_prop( $type, $prop, $value ), }; @@ -309,13 +312,31 @@ protected function set_base64_string_prop( string $prop, mixed $value ) { * @return void */ protected function set_json_prop( string $prop, string|array $value, bool $assoc = true ) { - \error_log( 'set_json_prop called with value: ' . \print_r( $value, true ) ); if ( ! \is_array( $value ) ) { $value = \json_decode( $value, $assoc ); } $this->set_wc_data_prop( $prop, $value ); } + protected function set_object_prop( string $prop, mixed $value ): void { + if ( \is_object( $value ) ) { + if ( ! \is_a( $value, XWC_Prop::class ) ) { + $this->error( 'invalid_object', 'Object must be an instance of XWC_Prop.' ); + } + + $this->set_wc_data_prop( $prop, $value ); + return; + } + + $value = null !== match ( true ) { + \json_decode( $value ) => \json_decode( $value, true ), + \is_a( $value, XWC_Prop::class, true ) => array( 'class' => $value ), + default => array(), + }; + + $this->set_wc_data_prop( $prop, XWC_Prop::from_json( $value ) ); + } + /** * Set an int prop * From 78c35b0f6307916fd327621b8daddb3161db97ac Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Thu, 12 Jun 2025 15:24:21 +0200 Subject: [PATCH 02/12] fix: Small fixes and QoL tweaks --- composer.json | 3 +- composer.lock | 78 ++++++++++- src/Core/XWC_Data.php | 61 ++++++++- src/Core/XWC_Data_Store_XT.php | 4 +- src/Core/XWC_Prop.php | 212 ++++++++++++++++++++++-------- src/Decorators/Model.php | 8 +- src/Decorators/Model_Modifier.php | 4 +- src/Entity.php | 4 +- src/Model/Prop_Getters.php | 94 +++++++++---- src/Model/Prop_Setters.php | 58 +++++--- 10 files changed, 409 insertions(+), 117 deletions(-) diff --git a/composer.json b/composer.json index c0e04e5..65c22fd 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "symfony/polyfill-php81": "^1.30", "x-wp/helper-classes": "^1", "x-wp/helper-functions": "^1", - "x-wp/di-implementation": "^1" + "x-wp/di-implementation": "^1", + "symfony/polyfill-php83": "^1.32" }, "require-dev": { "x-wp/di": "^1.0 || ^2.0", diff --git a/composer.lock b/composer.lock index 547943b..4cab2c2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ba617a63daade3fbd8e55a7b67456bda", + "content-hash": "169e0142ea35bc4d88c653fc5a108c4d", "packages": [ { "name": "automattic/jetpack-constants", @@ -375,6 +375,82 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "x-wp/di", "version": "v1.7.4", diff --git a/src/Core/XWC_Data.php b/src/Core/XWC_Data.php index 88f65bb..ac68171 100644 --- a/src/Core/XWC_Data.php +++ b/src/Core/XWC_Data.php @@ -47,6 +47,7 @@ abstract class XWC_Data extends WC_Data implements XWC_Data_Definition { * Data store object. * * @var XWC_Data_Store_XT + * @phpstan-ignore property.phpDocType */ protected $data_store; @@ -74,6 +75,25 @@ public function __construct( int|array|stdClass|XWC_Data $data = 0 ) { ->do_actions( $data ); } + /** + * Get the debug information for this object. + * + * @return array + */ + public function __debugInfo() { + return array( + 'changes' => $this->changes, + 'data' => $this->data, + 'id' => $this->get_id(), + 'meta_data' => wp_list_pluck( + $this->get_meta_data(), + 'value', + 'key', + ), + 'read' => $this->get_object_read(), + ); + } + /** * Universal prop getter / setter * @@ -135,6 +155,25 @@ public function get_core_data_read(): bool { return (bool) $this->core_read; } + /** + * Take the changes made to the meta props and apply them to the data. + * + * @return void + */ + public function apply_changes() { + $meta_changes = array_intersect( + array_keys( $this->changes ), + array_keys( array_diff_key( $this->data, $this->core_data, $this->extra_data, $this->tax_data ) ), + ); + + foreach ( $meta_changes as $meta_prop ) { + $this->data[ $meta_prop ] = $this->changes[ $meta_prop ]; + unset( $this->changes[ $meta_prop ] ); + } + + parent::apply_changes(); + } + public function save() { $args = $this->get_id() > 0 ? array( 'updated', 'changes' ) @@ -160,7 +199,7 @@ final protected function parse_method_name( string $name, array $args ): array { $type = $m[1] ?? ''; $prop = $m[2] ?? ''; - if ( ! $method || ! $type || ! $prop || ( 'set' === $type && ! isset( $args[0] ) ) ) { + if ( ! $method || ! $type || ! $prop || ( 'set' === $type && count( $args ) < 1 ) ) { $this->error( 'bmc', \sprintf( 'BMC: %s, %s', static::class, $name ) ); } @@ -338,28 +377,38 @@ protected function is_internal_meta_key( $key ) { } protected function maybe_set_object(): static { - if ( ! in_array( 'object', $this->get_prop_types(), true ) ) { + if ( ! $this->has_prop_type( 'object' ) ) { return $this; } $changed = array_diff( (array) $this->get_prop_by_type( 'object' ), - array_keys( $this->get_changes() ), + array_keys( parent::get_changes() ), ); foreach ( $changed as $prop ) { - $obj = $this->get_prop( $prop ); + $obj = $this->{"get_{$prop}"}(); - if ( ! $obj?->changed() ) { + if ( ! ( $obj?->changed() ?? false ) ) { continue; } - $this->set_prop( $prop, $obj ); + $this->changes[ $prop ] = $obj; } return $this; } + protected function has_prop_type( string $type ): bool { + foreach ( $this->get_prop_types() as $t ) { + if ( $t === $type || str_starts_with( $t, $type . '|' ) ) { + return true; + } + } + + return false; + } + /** * Maybe set the created or updated date on save. * diff --git a/src/Core/XWC_Data_Store_XT.php b/src/Core/XWC_Data_Store_XT.php index e718f5b..d70de1e 100644 --- a/src/Core/XWC_Data_Store_XT.php +++ b/src/Core/XWC_Data_Store_XT.php @@ -52,7 +52,7 @@ class XWC_Data_Store_XT extends WC_Data_Store_WP implements WC_Object_Data_Store * core_data: array, * data: array, * tax_data: array, - * prop_types: array, + * prop_types: array, * unique_data: array, * required_data: array, * } @@ -116,7 +116,7 @@ public function get_object_type(): string { * core_data: array, * data: array, * tax_data: array, - * prop_types: array, + * prop_types: array, * unique_data: array, * required_data: array, * } diff --git a/src/Core/XWC_Prop.php b/src/Core/XWC_Prop.php index 7b358c2..a9a9472 100644 --- a/src/Core/XWC_Prop.php +++ b/src/Core/XWC_Prop.php @@ -23,90 +23,72 @@ class XWC_Prop implements ArrayAccess, JsonSerializable { * * @var string */ - protected string $hash = ''; + protected string $hash; /** - * Gets the default JSON representation of the object. + * Did we read the object? * - * @return static + * @var bool */ - public static function default(): static { - return new static(); - } + protected bool $read = false; + + protected bool $changed = false; /** - * Creates a new instance from JSON data. + * Gets the default JSON representation of the object. * - * @template Tp of XWC_Prop - * @param null|false|array{class?: class-string, data?: array, hash?: string} $data The JSON data. - * @return Tp + * @return static */ - public static function from_json( null|bool|array $data ): XWC_Prop { - if ( ! is_array( $data ) || ! $data ) { - $data = array( - 'data' => array(), - 'hash' => '', - ); - } - $data['class'] ??= static::class; - - $cname = is_a( $data['class'], static::class, true ) ? $data['class'] : static::class; - - return new $data['class']( $data ); + public static function default(): static { + // @phpstan-ignore new.static + return new static(); } /** * Constructor. * - * @param array{data?: array, hash?: string} $args Data and hash. + * @param array $data Data and hash. * } */ - public function __construct( array $args = array() ) { - if ( isset( $args['data'] ) ) { - $this->sort( $args['data'] ); - } + public function __construct( array $data = array() ) { + $this->data = $this->default_data(); + $this->hash = $this->hash_data(); - $this->data = $args['data'] ?? array(); - $this->hash = $args['hash'] ?? $this->hash_data(); + $this->load_data( $data ); } /** * Serializes the object to an array. * - * @return array{data: array, hash: string} Data and hash. + * @return array */ public function __serialize(): array { - return array( - 'data' => $this->data, - 'hash' => $this->hash_data(), - ); + return $this->get_data(); } /** * Unserializes the object from an array. * - * @param array{data?: array, hash?: string} $data Data and hash. + * @param array $data Data to unserialize. */ public function __unserialize( array $data ): void { - $this->data = $data['data'] ?? array(); - $this->hash = $data['hash'] ?? $this->hash_data(); + $this->load_data( $data ); } /** * Return data needed for JSON serialization. * - * @return array{ - * class:class-string, - * data: array, - * hash: string, - * } + * @return ?array */ public function jsonSerialize(): mixed { - return array( - 'class' => static::class, - 'data' => $this->get_data(), - 'hash' => $this->hash_data(), - ); + $data = $this->get_data(); + $defl = $this->default_data(); + + $this->sort( $data ); + $this->sort( $defl ); + return $data === $defl + ? null + : $data; } /** @@ -117,8 +99,14 @@ public function jsonSerialize(): mixed { * @return static */ public function set( string $offset, mixed $value ): static { + $old = $this->get( $offset ); + $this->data[ $offset ] = $value; + if ( $this->get_read() && $old !== $value ) { + $this->changed = true; + } + return $this; } @@ -129,13 +117,46 @@ public function set( string $offset, mixed $value ): static { * @return static */ public function set_data( array $data ): static { - $this->sort( $data ); - + $old = $this->data; $this->data = $data; + if ( $this->get_read() && $old !== $data ) { + $this->changed = true; + } + return $this; } + /** + * Mark the object as read. + * + * @param bool $read Whether the object has been read. + * @return static + */ + public function set_read( bool $read = true ): static { + $this->read = $read; + + return $this; + } + + /** + * Sets the data for the property. + * + * @template TData of XWC_Prop + * + * @param TData|array $data The data to set. + * @return ($data is array ? static : TData) + */ + public function with_data( array|XWC_Prop $data ): XWC_Prop { + if ( is_array( $data ) ) { + return $this->set_data( $data ); + } + + $cname = $data::class; + + return new $cname( $data->get_data() ); + } + /** * Gets the value at the specified offset. * @@ -146,12 +167,39 @@ public function get( string $offset ): mixed { return $this->data[ $offset ] ?? null; } + /** + * Gets the data array. + * + * @return array + */ public function get_data(): array { return $this->data; } + /** + * Gets the hash of the data. + * + * @return ?string + */ + public function get_hash(): ?string { + return $this->hash ?? null; + } + + /** + * Checks if the object has been read. + * + * @return bool + */ + public function get_read(): bool { + return $this->read; + } + public function changed(): bool { - return array() !== $this->data && $this->hash !== $this->hash_data(); + if ( ! $this->get_read() ) { + return false; + } + + return $this->changed; } /** @@ -203,15 +251,53 @@ public function offsetUnset( $offset ): void { throw new \BadMethodCallException( 'Do not use this method directly. Use the setter method instead.' ); } - private function hash_data(): string { - $data = $this->data; + /** + * Sets the hash of the data. + * + * @param string $hash The hash to set. + * @return static + */ + protected function set_hash( string $hash ): static { + $this->hash = $hash; + + return $this; + } + + /** + * Returns the default data for this property. + * + * @return array + */ + protected function default_data(): array { + return array(); + } + + /** + * Hashes the data. + * + * This method sorts the data and then hashes it using md5. + * + * @param null|array $data Optional data to hash. If not provided, uses the current data. + * @return string The hash of the data. + */ + protected function hash_data( ?array $data = null ): string { + $data ??= $this->data; - ksort( $data ); + $this->sort( $data ); return hash( 'md5', (string) wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ); } - private function sort( array &$arr ): void { + /** + * Sorts the data array recursively. + * + * This method sorts the array in place, preserving the keys and ensuring + * that nested arrays are also sorted. + * + * @param array $arr The array to sort. + * @return void + */ + protected function sort( array &$arr ): void { array_is_list( $arr ) ? sort( $arr, SORT_NATURAL | SORT_FLAG_CASE ) : ksort( $arr, SORT_NATURAL | SORT_FLAG_CASE ); @@ -224,4 +310,22 @@ private function sort( array &$arr ): void { $this->sort( $value ); } } + + /** + * Load the data. + * + * @param array $data The data to load. + * } + */ + protected function load_data( array $data = array() ): void { + if ( ! $data ) { + $this->set_read( true ); + return; + } + + $this + ->set_data( $data ) + ->set_hash( $this->hash_data() ) + ->set_read( true ); + } } diff --git a/src/Decorators/Model.php b/src/Decorators/Model.php index cb66a6a..ced2ca2 100644 --- a/src/Decorators/Model.php +++ b/src/Decorators/Model.php @@ -47,7 +47,7 @@ class Model { * * @var array $core_props Array of core properties. * @param array|null $data_store Data store class name. * @param array + * @var array */ protected array $prop_types = array(); @@ -105,7 +105,7 @@ public function get_core_data( string $context = 'db', bool $include_id = false, */ public function get_core_changes(): array { $changed = array(); - $props = \array_intersect( $this->get_core_keys(), \array_keys( $this->changes ) ); + $props = \array_intersect( $this->get_core_keys(), \array_keys( $this->get_changes() ) ); if ( 0 === \count( $props ) ) { return $changed; @@ -135,12 +135,25 @@ public function get_data() { return $data; } + /** + * Get all changes for this object. + * + * This includes core data, extra data, and meta data. + * + * @return array + */ + public function get_changes() { + return $this + ->maybe_set_object() + ->get_wc_data_changes(); + } + /** * Get the type of a prop. * * @param string $prop Name of prop to get type for. * @return array{ - * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, + * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, * 1: array * } | array{0: 'enum', 1: array{0: class-string}} */ @@ -150,7 +163,7 @@ protected function get_prop_type( string $prop ): array { /** * Variable narrowing for prop types. * - * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string $type + * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string $type */ $type = \array_shift( $types ); @@ -179,7 +192,11 @@ protected function is_base64_string( ?string $value ): bool { * @return ($type is 'date_created' ? null|string : ($type is 'date_updated' ? null|string : null|string|array)) */ protected function get_prop_by_type( string $type ): null|string|array { - $types = \array_filter( $this->get_prop_types(), static fn( $t ) => $t === $type ); + $types = \array_filter( + $this->get_prop_types(), + fn( $t ) => $this->filter_prop( $t, $type ), + ); + $types = \array_keys( $types ); return match ( \count( $types ) ) { @@ -217,6 +234,7 @@ protected function get_prop( $prop, $context = 'view' ) { 'bool_int' => $this->get_bool_prop( $value, 'int' ), 'array_assoc' => $this->get_array_prop( $value, 'assoc' ), 'array' => $this->get_array_prop( $value, 'normal' ), + 'array_set' => $this->get_array_prop( $value, 'set' ), 'term_single' => $this->get_term_prop( $value, ...$sub ), 'term_array' => $this->get_term_prop( $value, ...$sub ), 'enum' => $this->get_enum_prop( $value ), @@ -224,11 +242,23 @@ protected function get_prop( $prop, $context = 'view' ) { 'json_obj' => $this->get_json_prop( $value, \JSON_FORCE_OBJECT ), 'binary' => $this->get_binary_prop( $value ), 'base64_string' => $this->get_base64_string_prop( $value ), - 'object' => $this->get_object_prop( $value, $prop ), + 'object' => $this->get_object_prop( $value, ...$sub ), default => $this->get_unknown_prop( $type, $prop, $value ), }; } + /** + * Get WC data changes. + * + * This is a wrapper for the parent method to ensure that the WC_Data + * changes are returned in the correct format. + * + * @return array + */ + protected function get_wc_data_changes(): array { + return parent::get_changes(); + } + protected function get_wc_data_prop( string $prop, string $context = 'view' ): mixed { return parent::get_prop( $prop, $context ); } @@ -259,7 +289,7 @@ protected function get_bool_prop( mixed $value, string $format = 'string' ): int * Get array prop value. * * @param mixed $value Value to convert to a string. - * @param 'assoc'|'normal' $format Format of the array. + * @param 'assoc'|'normal'|'set' $format Format of the array. * @return string */ protected function get_array_prop( mixed $value, string $format = 'assoc' ): string { @@ -267,6 +297,7 @@ protected function get_array_prop( mixed $value, string $format = 'assoc' ): str // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize 'assoc' => \serialize( $value ), 'normal' => \implode( ',', $value ), + 'set' => \implode( ',', \array_unique( (array) $value ) ), }; } @@ -290,11 +321,11 @@ protected function get_term_prop( mixed $value, string $field, string $taxonomy /** * Get enum prop value. * - * @param \BackedEnum $enum_val Enum value. - * @return string|int + * @param \BackedEnum|null $enum_val Enum value. + * @return null|string|int */ - protected function get_enum_prop( $enum_val ): string|int { - return $enum_val->value; + protected function get_enum_prop( $enum_val ): null|string|int { + return $enum_val?->value ?? null; } /** @@ -322,25 +353,26 @@ protected function get_base64_string_prop( ?string $value ): string { return ! $this->is_base64_string( $value ) ? \base64_encode( $value ) : $value; } - protected function get_object_prop( mixed $value, string $prop ): string { + /** + * Get an object prop value. + * + * This is used for objects that implement JsonSerializable or Stringable. + * + * @param mixed $value Value to convert to a string. + * @param string $cname Class name of the prop, defaults to XWC_Prop. + * @return ?string + */ + protected function get_object_prop( mixed $value, string $cname = \XWC_Prop::class ): ?string { $iof = static fn( $t ) => $t instanceof JsonSerializable || $t instanceof Stringable; + $enc = JSON_UNESCAPED_UNICODE; - if ( $iof( $value ) ) { - $value = $this->get_json_prop( $value, \JSON_FORCE_OBJECT | \JSON_UNESCAPED_UNICODE ); - } - - if ( \is_string( $value ) && \class_exists( $value ) ) { - $value = ''; - } - - $value = $value ?: \wp_json_encode( - array( - 'class' => $this->default_data[ $prop ], - 'data' => array(), - ), - ); - - return $value; + return match ( true ) { + $iof( $value ) => $this->get_json_prop( $value, $enc ), + $value === $cname => $this->get_json_prop( new $cname(), $enc ), + \class_exists( (string) $value ) => null, + '' === (string) $value => null, + default => null, + }; } protected function get_unknown_prop( string $type, string $prop, mixed $value ): mixed { @@ -360,4 +392,10 @@ protected function get_unknown_prop( string $type, string $prop, mixed $value ): */ return \apply_filters( "xwc_data_get_{$type}_prop", $value, $prop ); } + + private function filter_prop( string $type, string $find ): bool { + $regex = '/^' . \preg_quote( $find, '/' ) . '(?:$|\|.+$)/'; + + return 1 === \preg_match( $regex, $type ); + } } diff --git a/src/Model/Prop_Setters.php b/src/Model/Prop_Setters.php index 03a9434..fe8ffba 100644 --- a/src/Model/Prop_Setters.php +++ b/src/Model/Prop_Setters.php @@ -24,7 +24,7 @@ trait Prop_Setters { * * @param string $prop Name of prop to get type for. * @return array{ - * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, + * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, * 1: array * } | array{0: 'enum', 1: array{0: class-string}} */ @@ -105,6 +105,7 @@ protected function set_prop( $prop, $value ): static { 'term_array' => $this->set_array_term_prop( $prop, $value, ...$sub ), 'array_assoc' => $this->set_assoc_arr_prop( $prop, $value ), 'array' => $this->set_normal_arr_prop( $prop, $value ), + 'array_set' => $this->set_unique_arr_prop( $prop, $value ), 'binary' => $this->set_binary_prop( $prop, $value ), 'base64_string' => $this->set_base64_string_prop( $prop, $value ), 'json_obj' => $this->set_json_prop( $prop, $value, false ), @@ -113,7 +114,7 @@ protected function set_prop( $prop, $value ): static { 'float' => $this->set_float_prop( $prop, $value ), 'slug' => $this->set_slug_prop( $prop, $value ), 'string' => $this->set_wc_data_prop( $prop, $value ), - 'object' => $this->set_object_prop( $prop, $value ), + 'object' => $this->set_object_prop( $prop, $value, ...$sub ), default => $this->set_unknown_prop( $type, $prop, $value ), }; @@ -258,6 +259,19 @@ protected function set_normal_arr_prop( string $prop, $value ) { $this->set_wc_data_prop( $prop, \wc_string_to_array( $value ) ); } + /** + * Set an array prop with unique values + * + * @param string $prop Property name. + * @param mixed $value Property value. + * @return void + */ + protected function set_unique_arr_prop( string $prop, $value ) { + $value = \array_values( \array_unique( \wc_string_to_array( $value ) ) ); + + $this->set_wc_data_prop( $prop, $value ); + } + /** * Set an associative array prop * @@ -318,23 +332,33 @@ protected function set_json_prop( string $prop, string|array $value, bool $assoc $this->set_wc_data_prop( $prop, $value ); } - protected function set_object_prop( string $prop, mixed $value ): void { - if ( \is_object( $value ) ) { - if ( ! \is_a( $value, XWC_Prop::class ) ) { - $this->error( 'invalid_object', 'Object must be an instance of XWC_Prop.' ); - } - - $this->set_wc_data_prop( $prop, $value ); - return; - } - - $value = null !== match ( true ) { - \json_decode( $value ) => \json_decode( $value, true ), - \is_a( $value, XWC_Prop::class, true ) => array( 'class' => $value ), - default => array(), + /** + * Set an object prop + * + * @template TObj of XWC_Prop + * + * @param string $prop + * @param mixed $value + * @param class-string $cname Class name to parse the value into. + */ + protected function set_object_prop( string $prop, mixed $value, string $cname = XWC_Prop::class ): void { + $data = match ( true ) { + \is_array( $value ) => $value, + \is_string( $value ) => \json_decode( $value, true ) ?? array(), + \is_a( $value, XWC_Prop::class ) => $value, + default => array(), }; - $this->set_wc_data_prop( $prop, XWC_Prop::from_json( $value ) ); + /** + * If the object is not read, we need to get the prop from the data store. + * + * @var TObj $obj + */ + $obj = $this->get_object_read() + ? $this->get_prop( $prop )?->with_data( $data ) ?? new $cname( $data ) + : new $cname( $data ); + + $this->set_wc_data_prop( $prop, $obj ); } /** From ea411218e91febfbcd0ee2c760ff0d9afcd689fb Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Tue, 24 Jun 2025 14:15:27 +0200 Subject: [PATCH 03/12] fix: Various fixes and tweaks --- src/Entity.php | 55 ++++++++++++++++++++++++++++++------------ src/Entity_Manager.php | 7 +++++- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/Entity.php b/src/Entity.php index ed09d41..5821f41 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -87,6 +87,13 @@ class Entity { 'meta_obj_field', ); + /** + * Properties to be set. + * + * @var array + */ + private static array $props; + /** * Object factories. * @@ -140,7 +147,7 @@ class Entity { * * @var array + */ + private array $defaults = array( + 'meta_table' => '', + ); + /** * Constructor. * * @param Model ...$defs Model definitions. */ - public function __construct( - Model ...$defs, - ) { - $vars = \array_keys( \get_class_vars( $this::class ) ); - $vars = \array_diff( - $vars, - array( 'args', 'factories', 'stores', 'hooked', 'defaults', 'ctr', 'container' ), - ); - - foreach ( $vars as $var ) { - - $this->$var = $this->set_prop( $var, $defs ); + public function __construct( Model ...$defs ) { + foreach ( $this->get_props() as $prop ) { + $this->$prop = $this->set_prop( $prop, $defs ); } static::$stores[ $this->name ] = null; @@ -262,6 +269,10 @@ public function prime_data_store( array $stores ): array { protected function set_prop( string $prop, array $defs ): mixed { $defined = \wp_list_pluck( \wp_list_filter( $defs, array( $prop => null ), 'NOT' ), $prop ); + if ( ! \count( $defined ) ) { + return $this->defaults[ $prop ] ?? null; + } + if ( 1 === \count( $defined ) ) { return \current( $defined ); } @@ -272,9 +283,9 @@ protected function set_prop( string $prop, array $defs ): mixed { return \array_merge( $base, ...$defined ); } - $final = \end( $defined ); + $final = \count( $defined ) ? \end( $defined ) : $base; - return $final ? $final : $base; + return $final ?? $this->defaults[ $prop ] ?? null; } /** @@ -429,6 +440,18 @@ protected function get_repo(): XWC_Data_Store_XT { return $this->get_data_store(); } + /** + * Get the properties of the entity. + * + * @return array + */ + private function get_props(): array { + return self::$props ??= \array_diff( + \array_keys( \get_class_vars( $this::class ) ), + array( 'props', 'defaults', 'args', 'factories', 'stores', 'hooked', 'defaults', 'ctr', 'container' ), + ); + } + /** * Make an instance of a class. * diff --git a/src/Entity_Manager.php b/src/Entity_Manager.php index 2ead7ca..2cb755b 100644 --- a/src/Entity_Manager.php +++ b/src/Entity_Manager.php @@ -115,8 +115,13 @@ protected function do_register( string $classname, ?ContainerInterface $containe * @return array,XWC_Object_Factory,XWC_Meta_Store>> */ protected function get_models( string $target ): array { - $defs = array(); + /** + * Get the inheritance chain for the target class. + * + * @var array> $chain + */ $chain = Reflection::get_inheritance_chain( $target, true ); + $defs = array(); foreach ( $chain as $classname ) { $defs[] = Reflection::get_decorator( $classname, Model::class )?->set_model( $classname ); From ecd16129a6207ceb27678db2c0b2af08b6774111 Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sun, 6 Jul 2025 21:15:10 +0200 Subject: [PATCH 04/12] fix: Interface autoload fix --- composer.json | 3 +- composer.lock | 280 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 170 insertions(+), 113 deletions(-) diff --git a/composer.json b/composer.json index 65c22fd..7fc7047 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "XWC\\Data\\": "src/" }, "classmap": [ - "src/Core/" + "src/Core/", + "src/Interfaces/" ], "files": [ "lib/bootstrap.php", diff --git a/composer.lock b/composer.lock index 4cab2c2..12b4091 100644 --- a/composer.lock +++ b/composer.lock @@ -8,25 +8,26 @@ "packages": [ { "name": "automattic/jetpack-constants", - "version": "v2.0.5", + "version": "v3.0.8", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1" + "reference": "f9bf00ab48956b8326209e7c0baf247a0ed721c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/f9bf00ab48956b8326209e7c0baf247a0ed721c4", + "reference": "f9bf00ab48956b8326209e7c0baf247a0ed721c4", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "^1.1.1" + "automattic/jetpack-changelogger": "^6.0.5", + "automattic/phpunit-select-config": "^1.0.3", + "brain/monkey": "^2.6.2", + "yoast/phpunit-polyfills": "^4.0.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -36,7 +37,7 @@ "autotagger": true, "mirror-repo": "Automattic/jetpack-constants", "branch-alias": { - "dev-trunk": "2.0.x-dev" + "dev-trunk": "3.0.x-dev" }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}" @@ -53,22 +54,22 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.5" + "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.8" }, - "time": "2024-11-04T09:23:35+00:00" + "time": "2025-04-28T15:12:45+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.2", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44" + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2e1a362527783bcab6c316aad51bf36c5513ae44", - "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", "shasum": "" }, "require": { @@ -116,7 +117,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-01-24T15:42:37+00:00" + "time": "2025-03-19T13:51:03+00:00" }, { "name": "php-di/invoker", @@ -175,16 +176,16 @@ }, { "name": "php-di/php-di", - "version": "7.0.8", + "version": "7.0.11", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "98ddc81f8f768a2ad39e4cbe737285eaeabe577a" + "reference": "32f111a6d214564520a57831d397263e8946c1d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/98ddc81f8f768a2ad39e4cbe737285eaeabe577a", - "reference": "98ddc81f8f768a2ad39e4cbe737285eaeabe577a", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2", + "reference": "32f111a6d214564520a57831d397263e8946c1d2", "shasum": "" }, "require": { @@ -200,8 +201,8 @@ "friendsofphp/php-cs-fixer": "^3", "friendsofphp/proxy-manager-lts": "^1", "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^4.6" + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" }, "suggest": { "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" @@ -232,7 +233,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.8" + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11" }, "funding": [ { @@ -244,7 +245,7 @@ "type": "tidelift" } ], - "time": "2025-01-28T21:02:46+00:00" + "time": "2025-06-03T07:45:57+00:00" }, { "name": "psr/container", @@ -301,7 +302,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -357,7 +358,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -453,20 +454,20 @@ }, { "name": "x-wp/di", - "version": "v1.7.4", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/x-wp/di.git", - "reference": "21a26e76e405842460ab68f32dd4aedef0deef8a" + "reference": "ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/di/zipball/21a26e76e405842460ab68f32dd4aedef0deef8a", - "reference": "21a26e76e405842460ab68f32dd4aedef0deef8a", + "url": "https://api.github.com/repos/x-wp/di/zipball/ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa", + "reference": "ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2", + "automattic/jetpack-constants": "^2 || ^3", "php": ">=8.0", "php-di/php-di": "^7", "symfony/polyfill-php81": "^1.31", @@ -477,19 +478,23 @@ "oblak/wp-hook-di": "*" }, "provide": { - "psr/container-implementation": "^1.0", + "psr/container-implementation": "1.1 || 2.0", "x-wp/di-implementation": "self.version" }, "require-dev": { + "automattic/jetpack-autoloader": "*", "oblak/wordpress-coding-standard": "^1.1", + "php-stubs/woocommerce-stubs": "^9.5", "php-stubs/wordpress-stubs": "^6.6", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^1.12", "phpstan/phpstan-deprecation-rules": "^1.2", "swissspidy/phpstan-no-private": "^0.2", "symfony/polyfill-php82": "^1.31", + "symfony/var-dumper": "^5.4", "szepeviktor/phpstan-wordpress": "^1.3", - "wp-cli/wp-cli": "^2.11" + "wp-cli/wp-cli": "^2.11", + "x-wp/whoops": "^1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -528,22 +533,28 @@ ], "support": { "issues": "https://github.com/x-wp/hook-manager/issues", - "source": "https://github.com/x-wp/di/tree/v1.7.4" + "source": "https://github.com/x-wp/di/tree/v1.8.1" }, - "time": "2025-02-05T17:03:52+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-07-06T19:03:40+00:00" }, { "name": "x-wp/helper-classes", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-classes.git", - "reference": "a8d3424b875696c87d5dc26e73d66dbbb9992d74" + "reference": "9d0e4611a41846e5407c61548d0de2f7da9c7478" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-classes/zipball/a8d3424b875696c87d5dc26e73d66dbbb9992d74", - "reference": "a8d3424b875696c87d5dc26e73d66dbbb9992d74", + "url": "https://api.github.com/repos/x-wp/helper-classes/zipball/9d0e4611a41846e5407c61548d0de2f7da9c7478", + "reference": "9d0e4611a41846e5407c61548d0de2f7da9c7478", "shasum": "" }, "require": { @@ -584,22 +595,28 @@ ], "support": { "issues": "https://github.com/x-wp/helper-classes/issues", - "source": "https://github.com/x-wp/helper-classes/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-classes/tree/v1.21.0" }, - "time": "2025-02-05T15:40:56+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-02-11T18:24:14+00:00" }, { "name": "x-wp/helper-functions", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-functions.git", - "reference": "0430cff023ec47d99ed527501e5189ca799681c5" + "reference": "b6696bc39b68e3df1b9da77c7d4226347861045c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-functions/zipball/0430cff023ec47d99ed527501e5189ca799681c5", - "reference": "0430cff023ec47d99ed527501e5189ca799681c5", + "url": "https://api.github.com/repos/x-wp/helper-functions/zipball/b6696bc39b68e3df1b9da77c7d4226347861045c", + "reference": "b6696bc39b68e3df1b9da77c7d4226347861045c", "shasum": "" }, "require": { @@ -612,6 +629,7 @@ "autoload": { "files": [ "xwp-helper-fns-arr.php", + "xwp-helper-fns-meta.php", "xwp-helper-fns-num.php", "xwp-helper-fns-req.php", "xwp-helper-fns.php" @@ -642,22 +660,28 @@ ], "support": { "issues": "https://github.com/x-wp/helper-functions/issues", - "source": "https://github.com/x-wp/helper-functions/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-functions/tree/v1.21.0" }, - "time": "2025-02-01T19:59:36+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-06-08T13:14:33+00:00" }, { "name": "x-wp/helper-traits", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-traits.git", - "reference": "0367d136d6ba36e2ae0fe1854584ef760ea7cae9" + "reference": "c341b8ad27de9b1a73e2d53dc8702897a0cfba18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-traits/zipball/0367d136d6ba36e2ae0fe1854584ef760ea7cae9", - "reference": "0367d136d6ba36e2ae0fe1854584ef760ea7cae9", + "url": "https://api.github.com/repos/x-wp/helper-traits/zipball/c341b8ad27de9b1a73e2d53dc8702897a0cfba18", + "reference": "c341b8ad27de9b1a73e2d53dc8702897a0cfba18", "shasum": "" }, "require": { @@ -694,36 +718,42 @@ ], "support": { "issues": "https://github.com/x-wp/helper-traits/issues", - "source": "https://github.com/x-wp/helper-traits/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-traits/tree/v1.21.0" }, - "time": "2024-09-18T12:43:44+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-06-08T10:17:19+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -743,9 +773,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -753,7 +783,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -774,9 +803,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-06-27T17:24:01+00:00" }, { "name": "oblak/wordpress-coding-standard", @@ -840,25 +888,28 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.7.1", + "version": "v6.8.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1" + "reference": "92e444847d94f7c30f88c60004648f507688acd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/83448e918bf06d1ed3d67ceb6a985fc266a02fd1", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/92e444847d94f7c30f88c60004648f507688acd5", + "reference": "92e444847d94f7c30f88c60004648f507688acd5", "shasum": "" }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^5.4", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^1.11", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" @@ -882,9 +933,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.1" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.1" }, - "time": "2024-11-24T03:57:09+00:00" + "time": "2025-05-02T12:33:34+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1097,29 +1148,29 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd" + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/46d08eb86eec622b96c466adec3063adfed280dd", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.12.1" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -1175,33 +1226,33 @@ "type": "thanks_dev" } ], - "time": "2025-04-20T23:35:32+00:00" + "time": "2025-06-14T07:40:39+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -1238,6 +1289,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -1261,9 +1313,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2025-06-12T04:32:33+00:00" }, { "name": "phpstan/extension-installer", @@ -1362,16 +1418,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.16", + "version": "1.12.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e0bb5cb78545aae631220735aa706eac633a6be9", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", "shasum": "" }, "require": { @@ -1416,7 +1472,7 @@ "type": "github" } ], - "time": "2025-01-21T14:50:05+00:00" + "time": "2025-05-21T20:51:45+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -1467,32 +1523,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.18.0", + "version": "8.19.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593" + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f3b23cb9b26301b8c3c7bb03035a1bee23974593", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.12.2" + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "phing/phing": "3.0.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.13", - "phpstan/phpstan-deprecation-rules": "2.0.2", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.3" + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -1516,7 +1572,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.18.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" }, "funding": [ { @@ -1528,20 +1584,20 @@ "type": "tidelift" } ], - "time": "2025-05-01T09:40:50+00:00" + "time": "2025-06-09T17:53:57+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -1612,7 +1668,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { "name": "swissspidy/phpstan-no-private", @@ -1667,7 +1723,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -1723,7 +1779,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { From 4d65f60beb499ce0cc5d8f84afaca1708007005e Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 20:31:12 +0200 Subject: [PATCH 05/12] refactor: improve object factories and stateful data --- src/Core/XWC_Data.php | 28 ++- src/Core/XWC_Data_Store_XT.php | 12 +- src/Core/XWC_Object_Factory.php | 141 +++++---------- src/Decorators/Model.php | 8 +- src/Decorators/Model_Modifier.php | 8 +- src/Entity.php | 49 ++++-- src/Interfaces/Object_Factory.php | 44 +++++ src/Interfaces/XWC_Stateful_Data.php | 51 ++++++ src/Mixins/Status_Prop_Methods.php | 114 ++++++++++++ src/Mixins/Status_Transition_Methods.php | 213 +++++++++++++++++++++++ src/Model/Prop_Getters.php | 20 ++- src/Model/Prop_Setters.php | 55 +++--- src/Utils/xwc-data-utils-object.php | 13 +- 13 files changed, 593 insertions(+), 163 deletions(-) create mode 100644 src/Interfaces/Object_Factory.php create mode 100644 src/Interfaces/XWC_Stateful_Data.php create mode 100644 src/Mixins/Status_Prop_Methods.php create mode 100644 src/Mixins/Status_Transition_Methods.php diff --git a/src/Core/XWC_Data.php b/src/Core/XWC_Data.php index ac68171..9ff9434 100644 --- a/src/Core/XWC_Data.php +++ b/src/Core/XWC_Data.php @@ -111,10 +111,32 @@ public function __call( string $name, array $args ): mixed { : $this->set_prop( $prop, $args[0] ); } + /** + * Serialize the object. + * + * @return array{id: int} + */ + public function __serialize(): array { + return array( 'id' => $this->get_id() ); + } + + /** + * Unserialize the object. + * + * @param array{id?: int} $data Data to unserialize. + */ + public function __unserialize( array $data ): void { + $this + ->load_data_store() + ->load_object_args() + ->load_data( $data['id'] ?? 0 ) + ->do_actions( $data['id'] ?? 0 ); + } + public function jsonSerialize(): mixed { $data = $this->get_data(); - unset( $data['meta_data'], $data['stats'] ); + unset( $data['meta_data'] ); return $data; } @@ -190,7 +212,7 @@ public function save() { * * @param string $name Method name. * @param array $args Method arguments. - * @return array{0: string, 1: string, 2: string}} + * @return array{0: string, 1: string, 2: string} */ final protected function parse_method_name( string $name, array $args ): array { \preg_match( '/^([gs]et)_(.+)$/', $name, $m ); @@ -419,7 +441,7 @@ protected function has_prop_type( string $type ): bool { protected function maybe_set_date( string $type, ?string $key = null ): static { $prop = $this->get_prop_by_type( "date_{$type}" ); - if ( ! $prop ) { + if ( ! $prop || \is_array( $prop ) ) { return $this; } diff --git a/src/Core/XWC_Data_Store_XT.php b/src/Core/XWC_Data_Store_XT.php index d70de1e..fe8d8b1 100644 --- a/src/Core/XWC_Data_Store_XT.php +++ b/src/Core/XWC_Data_Store_XT.php @@ -365,14 +365,14 @@ public function is_value_unique( mixed $value, string $prop, int $obj_id ): bool public function unique_entity_slug( string $slug, string $prop, int $obj_id ): string { global $wpdb; - $prop = $this->get_cols_to_props()[ $prop ] ?? $prop; + $col = $this->get_cols_to_props()[ $prop ] ?? $prop; $check = $wpdb->get_var( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i = %s AND %i != %d LIMIT 1', - $prop, + $col, $this->get_table(), - $prop, + $col, $slug, $this->get_id_field(), $obj_id, @@ -390,7 +390,8 @@ public function unique_entity_slug( string $slug, string $prop, int $obj_id ): s ? \preg_replace( '/-(\d+)$/', "-$suffix", $slug ) : "{$slug}-1"; - return $this->unique_entity_slug( $slug, $prop, $obj_id ); + // Pass the already-remapped column name to avoid double-remapping on recursion. + return $this->unique_entity_slug( (string) $slug, $col, $obj_id ); } /** @@ -467,8 +468,9 @@ protected function get_data_row( int $id ): array { $data_row = $wpdb->get_row( $wpdb->prepare( - "SELECT * FROM %i WHERE {$this->get_id_field()} = %d", + 'SELECT * FROM %i WHERE %i = %d', $this->get_table(), + $this->get_id_field(), $id, ), ARRAY_A, diff --git a/src/Core/XWC_Object_Factory.php b/src/Core/XWC_Object_Factory.php index d02bd1d..95b1285 100644 --- a/src/Core/XWC_Object_Factory.php +++ b/src/Core/XWC_Object_Factory.php @@ -1,6 +1,8 @@ |false get_object_classname(int $id, string $type) Get the object class name. - * @method static int|false get_object_id(mixed $id, string $type) Get the object ID. - * - * Object factory. + * @implements Object_Factory */ -class XWC_Object_Factory { - use Singleton_Ex; - +class XWC_Object_Factory implements Object_Factory { /** - * Array of entity names with their class names. + * Entity type. * - * @var array> + * @var string */ - protected array $models = array(); + protected string $type; /** - * Constructor + * Data object class name. + * + * @var class-string */ - protected function __construct() { - $this->init(); - } + protected string $classname; /** - * Handles dynamic method calls for getting object data. + * Initialize the object factory with an entity. * - * @param string $name Method name. - * @param mixed $args Method arguments. + * @template D of XWC_Data_Store_XT + * @template M of XWC_Meta_Store * - * @return mixed + * @param Entity $e Entity instance. + * @return static */ - public function __call( string $name, $args ) { - \preg_match( '/^get_(.*?)(?:_id|_classname)?$/', $name, $matches ); + public function initialize( Entity $e ): static { + $this->type = $e->name; + $this->classname = $e->model; - $type = $matches[1] ?? ''; - $method = \str_replace( $type, 'object', $name ); + return $this; + } - if ( ! \method_exists( $this, $method ) ) { - return false; + public function make_object( mixed $id ): XWC_Data { + $obj = $this->get_object( $id ); + + if ( $obj ) { + return $obj; } - $args[] = $type; + $classname = $this->get_classname( 0 ); - return $this->{"$method"}( ...$args ); - } + if ( ! $classname ) { + throw new \RuntimeException( + \esc_html( "Cannot resolve a concrete class for entity type '{$this->type}'." ), + ); + } - /** - * Handles dynamic static method calls for getting object data - * - * @param string $name Method name. - * @param mixed $args Method arguments. - * - * @return mixed - */ - public static function __callStatic( $name, $args ) { - return static::instance()->__call( $name, $args ); + return new $classname( 0 ); } - /** - * Get a data object - * - * @param mixed $id Object ID. - * @param string $type Object type. - * @return T|false - */ - public function get_object( mixed $id, string $type = '' ): XWC_Data|bool { - $id = $this->{"get_{$type}_id"}( $id ); + public function get_object( mixed $id ): ?XWC_Data { + $id = $this->get_id( $id ); if ( ! $id ) { - return false; + return null; } - /** - * Filters the class name of a data object. - * - * @var class-string $classname - */ - $classname = $this->{"get_{$type}_classname"}( $id ); + $classname = $this->get_classname( $id ); try { return new $classname( $id ); } catch ( \Exception ) { - return false; - } - } - - /** - * - * Initialize the data types. - * - * @global Entity_Manager $xwc_entities - */ - protected function init(): void { - /** - * Global entity manager. - * - * @var Entity_Manager $xwc_entities - */ - global $xwc_entities; - - foreach ( $xwc_entities->get_entity() as $type => $dto ) { - $this->models[ $type ] = $dto->model; + return null; } } - /** - * Get the object ID. - * - * @param mixed $id Object ID. - * @param string $type Object type. - * @return int|false - */ - protected function get_object_id( mixed $id, string $type ): int|bool { - $obj = $GLOBALS[ $type ] ?? null; + public function get_id( mixed $id ): int|bool { + $obj = $GLOBALS[ $this->type ] ?? null; + // @phpstan-ignore return.type return match ( true ) { - default => false, \is_numeric( $id ) => (int) $id, - $obj instanceof XWC_Data => $obj->get_id(), $id instanceof XWC_Data => $id->get_id(), + $obj instanceof XWC_Data => $obj->get_id(), + default => false, }; } - /** - * Get the object class name. - * - * @param int $id Object ID. - * @param string $type Object type. - * @return class-string|false - */ - protected function get_object_classname( int $id, string $type ): string|bool { + public function get_classname( int $id ): bool|string { /** * Filters the class name of a data object. * * @var class-string|false $classname */ // Documented in WooCommerce. - $classname = \apply_filters( "xwc_{$type}_class", $this->models[ $type ] ?? false, $id ); + $classname = \apply_filters( "xwc_{$this->type}_class", $this->classname, $id ); if ( ! $classname || ! \class_exists( $classname ) ) { return false; diff --git a/src/Decorators/Model.php b/src/Decorators/Model.php index ced2ca2..cc26b9f 100644 --- a/src/Decorators/Model.php +++ b/src/Decorators/Model.php @@ -40,7 +40,7 @@ class Model { public string $table; public string $data_store; - public string $factory; + public ?string $factory; /** * Core properties. @@ -256,11 +256,11 @@ protected function set_data_store( ?string $store ): string { * Set the object factory class name. * * @param class-string|null $factory Object factory class name. - * @return ($factory is null ? class-string> : class-string) + * @return class-string|null */ - protected function set_factory( ?string $factory ): string { + protected function set_factory( ?string $factory ): ?string { if ( \is_null( $factory ) ) { - return XWC_Object_Factory::class; + return null; } if ( ! \class_exists( $factory ) ) { diff --git a/src/Decorators/Model_Modifier.php b/src/Decorators/Model_Modifier.php index 3fc2025..78d3c4d 100644 --- a/src/Decorators/Model_Modifier.php +++ b/src/Decorators/Model_Modifier.php @@ -84,9 +84,11 @@ public function __construct( protected function scaffold( array $args ): void { foreach ( $this->get_definers() as $prop => $setter ) { - $this->$prop = $args[ $prop ] || \is_null( $args[ $prop ] ) - ? $this->$setter( $args[ $prop ] ) - : $args[ $prop ]; + if ( ! \array_key_exists( $prop, $args ) ) { + continue; + } + + $this->$prop = $this->$setter( $args[ $prop ] ); } } } diff --git a/src/Entity.php b/src/Entity.php index 5821f41..23eea1a 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -140,7 +140,7 @@ class Entity { */ protected string $container; - protected string $factory; + protected ?string $factory; /** * Core properties. @@ -211,7 +211,7 @@ public function __construct( Model ...$defs ) { $this->$prop = $this->set_prop( $prop, $defs ); } - static::$stores[ $this->name ] = null; + static::$stores[ $this->name ] ??= null; // @phpstan-ignore assign.propertyType } /** @@ -249,7 +249,7 @@ public function add_hooks(): void { * Prime the data store with the entity name. * * @param array $stores Data stores. - * @return array + * @return array */ public function prime_data_store( array $stores ): array { $to_add = \array_keys( static::$stores ); @@ -267,7 +267,15 @@ public function prime_data_store( array $stores ): array { * @return mixed */ protected function set_prop( string $prop, array $defs ): mixed { - $defined = \wp_list_pluck( \wp_list_filter( $defs, array( $prop => null ), 'NOT' ), $prop ); + $defined = array(); + + foreach ( $defs as $def ) { + if ( ! isset( $def->$prop ) ) { + continue; + } + + $defined[] = $def->$prop; + } if ( ! \count( $defined ) ) { return $this->defaults[ $prop ] ?? null; @@ -283,9 +291,7 @@ protected function set_prop( string $prop, array $defs ): mixed { return \array_merge( $base, ...$defined ); } - $final = \count( $defined ) ? \end( $defined ) : $base; - - return $final ?? $this->defaults[ $prop ] ?? null; + return \end( $defined ); } /** @@ -303,7 +309,7 @@ protected function get_core_data(): array { * @return array */ protected function get_data(): array { - return \wp_list_pluck( $this->meta_props, 'default' ); + return \wp_list_pluck( $this->meta_props ?? array(), 'default' ); } /** @@ -312,7 +318,7 @@ protected function get_data(): array { * @return array */ protected function get_tax_data(): array { - return \wp_list_pluck( $this->tax_props, 'default' ); + return \wp_list_pluck( $this->tax_props ?? array(), 'default' ); } /** @@ -323,8 +329,8 @@ protected function get_tax_data(): array { protected function get_prop_types(): array { return \array_merge( \wp_list_pluck( $this->core_props, 'type' ), - \wp_list_pluck( $this->meta_props, 'type' ), - \wp_list_pluck( $this->tax_props, 'type' ), + \wp_list_pluck( $this->meta_props ?? array(), 'type' ), + \wp_list_pluck( $this->tax_props ?? array(), 'type' ), ); } @@ -357,7 +363,7 @@ protected function get_required_data(): array { * @return array */ protected function get_meta_to_props(): array { - return \array_flip( \wp_list_pluck( $this->meta_props, 'name' ) ); + return \array_flip( \wp_list_pluck( $this->meta_props ?? array(), 'name' ) ); } /** @@ -375,7 +381,7 @@ protected function get_cols_to_props(): array { * @return array */ protected function get_tax_to_props(): array { - return \array_flip( \wp_list_pluck( $this->tax_props, 'taxonomy' ) ); + return \array_flip( \wp_list_pluck( $this->tax_props ?? array(), 'taxonomy' ) ); } /** @@ -384,7 +390,7 @@ protected function get_tax_to_props(): array { * @return array */ protected function get_tax_fields(): array { - return \wp_list_pluck( $this->tax_props, 'field', 'taxonomy' ); + return \wp_list_pluck( $this->tax_props ?? array(), 'field', 'taxonomy' ); } /** @@ -393,7 +399,14 @@ protected function get_tax_fields(): array { * @return TFact */ protected function get_factory(): XWC_Object_Factory { - return static::$factories[ $this->name ] ??= $this->factory::instance(); + /** + * Variable override. + * + * @var null|class-string $factory + */ + $factory = $this->factory ?? XWC_Object_Factory::class; + + return static::$factories[ $this->name ] ??= $this->make( $factory )->initialize( $this ); } /** @@ -417,7 +430,7 @@ protected function get_meta_store(): ?XWC_Meta_Store { } protected function get_has_meta(): bool { - return '' !== $this->meta_table && array() !== $this->meta_props; + return '' !== $this->meta_table && ! empty( $this->meta_props ); } /** @@ -448,14 +461,14 @@ protected function get_repo(): XWC_Data_Store_XT { private function get_props(): array { return self::$props ??= \array_diff( \array_keys( \get_class_vars( $this::class ) ), - array( 'props', 'defaults', 'args', 'factories', 'stores', 'hooked', 'defaults', 'ctr', 'container' ), + array( 'props', 'defaults', 'args', 'factories', 'stores', 'hooked', 'ctr', 'container' ), ); } /** * Make an instance of a class. * - * @template TObj of TDstr|TMeta + * @template TObj of TDstr|TMeta|TFact * @param class-string $cname Class name. * @return TObj */ diff --git a/src/Interfaces/Object_Factory.php b/src/Interfaces/Object_Factory.php new file mode 100644 index 0000000..98fa9a8 --- /dev/null +++ b/src/Interfaces/Object_Factory.php @@ -0,0 +1,44 @@ + + */ + public function get_id( mixed $id ): int|bool; + + /** + * Get the class name of a data object by ID. + * + * @param int $id Object ID. + * @return false|class-string + */ + public function get_classname( int $id ): bool|string; +} diff --git a/src/Interfaces/XWC_Stateful_Data.php b/src/Interfaces/XWC_Stateful_Data.php new file mode 100644 index 0000000..4f1a749 --- /dev/null +++ b/src/Interfaces/XWC_Stateful_Data.php @@ -0,0 +1,51 @@ + + */ + abstract protected function get_valid_statuses(): array; + + abstract protected function get_default_status(): string; + + abstract protected function get_status_prefix(): string; + + /** + * Set the object status. + * + * @param string $to New status. + * @return array{from: string, to: string} + */ + public function set_status( string $to ): array { + $from = $this->get_status(); + $to = $this->strip_object_status( $to ); + + if ( ! $this->object_read ) { + $this->set_prop( 'status', $to ); + + return \compact( 'from', 'to' ); + } + + if ( ! $this->is_valid_status( $to ) ) { + $to = $this->get_default_status(); + } + + if ( 'draft' !== $from && ! $this->is_valid_status( $from ) ) { + $from = $this->get_default_status(); + } + + $this->set_prop( 'status', $to ); + + return \compact( 'from', 'to' ); + } + + /** + * Get the invoice status. + * + * @param string $context Context. + * @return string + */ + public function get_status( string $context = 'view' ): string { + $status = $this->get_prop( 'status', $context ); + + if ( '' === $status && 'view' === $context ) { + $status = $this->get_default_status(); + } + + return $status; + } + + /** + * Get invoice status prop for the database. + * + * @param string $status Status to format. + * @return string + */ + protected function get_object_status_prop( string $status ) { + if ( '' === $status ) { + $status = $this->get_default_status(); + } + + $status = $this->format_object_status( $status ); + + return $status; + } + + protected function strip_object_status( string $status ): string { + $prefix = $this->get_status_prefix(); + + return \str_starts_with( $status, $prefix ) + ? \substr( $status, \strlen( $prefix ) ) + : $status; + } + + /** + * Format the object status with a prefix. + * + * @param string $status Status to format. + * @return string + */ + protected function format_object_status( string $status ): string { + $prefix = $this->get_status_prefix(); + + return ! \str_starts_with( $status, $prefix ) + ? $prefix . $status + : $status; + } + + /** + * Check if a status is valid. + * + * @param string $status Status to check. + * @return bool + */ + protected function is_valid_status( string $status ): bool { + return \in_array( $this->format_object_status( $status ), $this->get_valid_statuses(), true ); + } +} diff --git a/src/Mixins/Status_Transition_Methods.php b/src/Mixins/Status_Transition_Methods.php new file mode 100644 index 0000000..bd88137 --- /dev/null +++ b/src/Mixins/Status_Transition_Methods.php @@ -0,0 +1,213 @@ +status_transition()->get_id(); + } + + /** + * Check if the object has a specific status. + * + * @param string ...$status Status. + * @return bool + */ + public function has_status( string ...$status ): bool { + return \in_array( $this->get_status(), $status, true ); + } + + /** + * Set the status of the object. + * + * @param string $new_status New status to set. + * @param string $note Note to add to the transition. + * @param bool $manual Whether the status change is manual or not. + * @return array{from: string, to: string} + */ + public function set_status( string $new_status, string $note = '', bool $manual = false ): array { + [ 'from' => $from, 'to' => $to ] = $this->set_status_base( $new_status ); + + if ( ! $this->object_read || '' === $from || $from === $to ) { + return array( + 'from' => $from, + 'to' => $to, + ); + } + + $from = $this->transition['from'] ?? $from; + + $this->transition = array( + 'from' => $from, + 'manual' => $manual, + 'note' => $note, + 'to' => $to, + ); + + if ( $manual ) { + $tag = $this->get_tag_base( 'edit', 'status' ); + + /** + * Fires when the status of an object is manually changed. + * + * @param int $id Object ID. + * @param string $to New status. + * + * @since 1.0.0 + */ + \do_action( $tag, $this->get_id(), $to ); + } + + return array( + 'from' => $from, + 'to' => $to, + ); + } + + /** + * Update the status of the object. + * + * Same as `set_status`, but also saves the object. + * + * @param string $new_status New status to set. + * @param string $note Note to add to the transition. + * @param bool $manual Whether the status change is manual or not. + * @return bool + */ + public function update_status( string $new_status, string $note = '', bool $manual = false ): bool { + if ( ! $this->can_update_status() ) { + return false; + } + + try { + $this->set_status( $new_status, $note, $manual ); + $this->save(); + } catch ( \Throwable ) { + return false; + } + + return true; + } + + protected function can_update_status(): bool { + return $this->get_id() > 0; + } + + /** + * Get the state transition for a specific property. + * + * @return false|array{ + * from: string, + * to: string, + * note: string, + * manual: bool + * } + */ + protected function get_transition(): bool|array { + $transition = $this->transition; + + $this->transition = false; + + return $transition; + } + + protected function status_transition(): static { + $transition = $this->get_transition(); + + if ( false === $transition ) { + return $this; + } + + $base = $this->get_tag_base(); + + try { + + [ 'from' => $from, 'to' => $to ] = $transition; + + /** + * Fires for specific status transition. + * + * @param int $id Object ID. + * @param XWC_Stateful_Data $object Object instance. + * @param array $transition Transition data. + * + * @since 1.0.0 + */ + \do_action( "{$base}_{$to}", $this->get_id(), $this, $transition ); + + /** + * Fires for status transition from one status to another. + * + * @param int $id Object ID. + * @param XWC_Stateful_Data $object Object instance. + * + * @since 1.0.0 + */ + \do_action( "{$base}_{$from}_to_{$to}", $this->get_id(), $this ); + + /** + * Fires for status change. + * + * @param int $id Object ID. + * @param string $from Previous status. + * @param string $to New status. + * @param XWC_Stateful_Data $object Object instance. + * + * @since 1.0.0 + */ + \do_action( "{$base}_changed", $this->get_id(), $from, $to, $this ); + + } catch ( \Exception $e ) { + if ( \function_exists( 'wc_get_logger' ) ) { + $logger = \wc_get_logger(); + + $logger->error( + \sprintf( + 'Status transition of %s #%d errored!', + $this->object_type, + $this->get_id(), + ), + array( + $this->object_type => $this, + 'error' => $e, + ), + ); + } + } finally { + return $this; + } + } + + protected function get_tag_base( string ...$tags ): string { + $tags = $tags ?: array( 'status' ); + \array_unshift( $tags, 'xwc', $this->object_type ); + + return \implode( '_', $tags ); + } +} diff --git a/src/Model/Prop_Getters.php b/src/Model/Prop_Getters.php index 5e22b96..576f73a 100644 --- a/src/Model/Prop_Getters.php +++ b/src/Model/Prop_Getters.php @@ -50,10 +50,12 @@ trait Prop_Getters { protected array $required_data = array(); public function get_prop_group( string $prop ): string { + $meta_props = \array_diff_key( $this->data, $this->core_data, $this->extra_data, $this->tax_data ); + return match ( true ) { isset( $this->core_data[ $prop ] ) => 'core', isset( $this->extra_data[ $prop ] ) => 'extra', - isset( $this->meta_data[ $prop ] ) => 'meta', + isset( $meta_props[ $prop ] ) => 'meta', default => 'none', }; } @@ -268,7 +270,7 @@ protected function get_date_prop( ?\WC_DateTime $value ): ?string { return null; } - return \gmdate( 'Y-m-d H:i:s', $value->getOffsetTimestamp() ); + return \gmdate( 'Y-m-d H:i:s', $value->getTimestamp() ); } /** @@ -346,10 +348,20 @@ protected function get_json_prop( mixed $value, int $flags = 0 ): string { * @return string */ protected function get_binary_prop( mixed $value ): string { - return ! $this->is_binary_string( $value ) ? \hex2bin( $value ) : $value; + if ( $this->is_binary_string( $value ) ) { + return $value; + } + + $decoded = \hex2bin( (string) $value ); + + return false !== $decoded ? $decoded : (string) $value; } - protected function get_base64_string_prop( ?string $value ): string { + protected function get_base64_string_prop( ?string $value ): ?string { + if ( null === $value ) { + return null; + } + return ! $this->is_base64_string( $value ) ? \base64_encode( $value ) : $value; } diff --git a/src/Model/Prop_Setters.php b/src/Model/Prop_Setters.php index fe8ffba..a754115 100644 --- a/src/Model/Prop_Setters.php +++ b/src/Model/Prop_Setters.php @@ -19,6 +19,16 @@ * @phpstan-require-extends XWC_Data */ trait Prop_Setters { + /** + * Whether we are currently inside a set_time_prop → parent::set_date_prop recursion. + * + * Using an instance property instead of a static variable so that concurrent + * calls on different props (or different object instances) cannot interfere. + * + * @var bool + */ + private bool $time_prop_loop = false; + /** * Get the type of a prop. * @@ -30,9 +40,9 @@ trait Prop_Setters { */ abstract protected function get_prop_type( string $prop ): array; - abstract protected function is_binary_string( string $value ): bool; + abstract protected function is_binary_string( ?string $value ): bool; - abstract protected function is_base64_string( string $value ): bool; + abstract protected function is_base64_string( ?string $value ): bool; /** * Set a collection of props in one go, collect any errors, and return the result. @@ -52,22 +62,15 @@ public function set_props( $props, $context = 'set' ) { return $prop_res; } - $save_res = null; - try { $save_res = $this->save(); } catch ( \Throwable $e ) { - $save_res = new \WP_Error( 'save_error', $e->getMessage() ); - } finally { - return match ( true ) { - 0 === $save_res => new \WP_Error( - 'save_error', - 'An unknown error occurred while saving.', - ), - \is_wp_error( $save_res ) => $save_res, - default => $this, - }; + return new \WP_Error( 'save_error', $e->getMessage() ); } + + return 0 === $save_res + ? new \WP_Error( 'save_error', 'An unknown error occurred while saving.' ) + : $this; } /** @@ -95,9 +98,9 @@ protected function set_prop( $prop, $value ): static { [ $type, $sub ] = $this->get_prop_type( $prop ); match ( $type ) { - 'date_created' => $this->set_date_prop( $prop, $value ), - 'date_updated' => $this->set_date_prop( $prop, $value ), - 'date' => $this->set_date_prop( $prop, $value ), + 'date_created' => $this->set_time_prop( $prop, $value ), + 'date_updated' => $this->set_time_prop( $prop, $value ), + 'date' => $this->set_time_prop( $prop, $value ), 'bool' => $this->set_bool_prop( $prop, $value ), 'bool_int' => $this->set_bool_prop( $prop, $value ), 'enum' => $this->set_enum_prop( $prop, $value, ...$sub ), @@ -154,17 +157,18 @@ protected function set_wc_data_prop( $prop, $value ) { * @param mixed $value Property value. * @return void */ - protected function set_date_prop( $prop, $value ) { - static $loop; + protected function set_time_prop( $prop, $value ) { + if ( ! $this->time_prop_loop ) { + if ( \is_string( $value ) && \preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value ) ) { + $value = \wc_string_to_timestamp( $value ); + } - if ( ! $loop ) { - $loop = true; + $this->time_prop_loop = true; parent::set_date_prop( $prop, $value ); + $this->time_prop_loop = false; return; } - $loop = false; - $this->set_wc_data_prop( $prop, $value ); } @@ -194,6 +198,11 @@ protected function set_bool_prop( string $prop, $value ) { * @return void */ protected function set_enum_prop( string $prop, mixed $val, null|string|BackedEnum $type = null ) { + if ( null === $type ) { + $this->set_wc_data_prop( $prop, $val ); + return; + } + if ( $val instanceof $type ) { $this->set_wc_data_prop( $prop, $val ); return; diff --git a/src/Utils/xwc-data-utils-object.php b/src/Utils/xwc-data-utils-object.php index 721bc15..a453166 100644 --- a/src/Utils/xwc-data-utils-object.php +++ b/src/Utils/xwc-data-utils-object.php @@ -70,18 +70,19 @@ function xwc_get_object( mixed $id, string $name, int|bool|null $def = false ): return $def; } - return xwc_get_object_factory( $name )->{"get_$name"}( $id ) ?: $def; + // @phpstan-ignore return.type + return xwc_get_object_factory( $name )->get_object( $id ) ?: $def; } /** * Get the class name of a data object by ID and type. * * @param int $id Object ID. - * @param string $name Object type. + * @param string $type Object type. * @return class-string */ -function xwc_get_object_classname( int $id, string $name ): string { - return xwc_get_object_factory( $name )->{"get_{$name}_classname"}( $id ); +function xwc_get_object_classname( int $id, string $type ): string { + return xwc_get_object_factory( $type )->get_classname( $id ) ?: XWC_Data::class; } /** @@ -92,9 +93,7 @@ function xwc_get_object_classname( int $id, string $name ): string { * @return XWC_Data */ function xwc_get_object_instance( int $id, string $type ): XWC_Data { - $classname = xwc_get_object_classname( $id, $type ); - - return new $classname( $id ); + return xwc_get_object_factory( $type )->make_object( $id ); } /** From 1532ca2ef0248bafe3d6a6714abdbeaedda06ce7 Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 20:46:05 +0200 Subject: [PATCH 06/12] chore: ignore local worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 22d0d82..bac75f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ vendor +.worktrees From 157b5f7f4b66fa78dfdbc9a8b763a92fe6297611 Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 21:29:59 +0200 Subject: [PATCH 07/12] test: add wordpress phpunit harness --- .github/workflows/tests.yml | 47 + .gitignore | 2 + bin/clean-wp-tests.sh | 7 + bin/install-wp-tests.sh | 216 ++ composer.json | 14 +- composer.lock | 1972 ++++++++++++++++- docker-compose.yml | 18 + phpunit.xml.dist | 19 + tests/SmokeTest.php | 18 + tests/bootstrap.php | 47 + .../xwc-data-type-tests.php | 9 + 11 files changed, 2312 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100755 bin/clean-wp-tests.sh create mode 100755 bin/install-wp-tests.sh create mode 100644 docker-compose.yml create mode 100644 phpunit.xml.dist create mode 100644 tests/SmokeTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4aa7549 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + - beta + - feat/** + +jobs: + phpunit: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + + - name: Install WP-CLI if missing + run: | + if command -v wp >/dev/null 2>&1; then + wp --info + exit 0 + fi + + curl -fsSL -o /tmp/wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + chmod +x /tmp/wp-cli.phar + sudo mv /tmp/wp-cli.phar /usr/local/bin/wp + wp --info + + - name: Start test services + run: composer test:env:start + + - name: Install WordPress test environment + run: composer test:install + + - name: Run PHPUnit + run: composer test diff --git a/.gitignore b/.gitignore index bac75f2..23b345f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ vendor .worktrees +.cache +.phpunit.result.cache diff --git a/bin/clean-wp-tests.sh b/bin/clean-wp-tests.sh new file mode 100755 index 0000000..d9080c5 --- /dev/null +++ b/bin/clean-wp-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +rm -rf "${REPO_ROOT}/.cache/wp-tests" diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..bf6a5d4 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CACHE_DIR="${REPO_ROOT}/.cache" +WP_CORE_DIR="${WP_CORE_DIR:-${CACHE_DIR}/wp-tests/wordpress}" +WP_TESTS_DIR="${WP_TESTS_DIR:-${CACHE_DIR}/wp-tests/lib}" +WP_DOWNLOADS_DIR="${CACHE_DIR}/wp-tests/downloads" +WP_DEVELOP_DIR="${CACHE_DIR}/wp-tests/wordpress-develop" +PLUGIN_SLUG="xwc-data-type-tests" +PLUGIN_SOURCE_DIR="${REPO_ROOT}/tests/wp-plugin/${PLUGIN_SLUG}" +PLUGIN_TARGET_DIR="${WP_CORE_DIR}/wp-content/plugins/${PLUGIN_SLUG}" + +DB_NAME="${DB_NAME:-wordpress_test}" +DB_USER="${DB_USER:-wordpress}" +DB_PASSWORD="${DB_PASSWORD:-wordpress}" +DB_HOST="${DB_HOST:-127.0.0.1}" +DB_PORT="${DB_PORT:-33067}" +DB_ROOT_USER="${DB_ROOT_USER:-root}" +DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-root}" + +WP_VERSION="${WP_VERSION:-latest}" +WC_VERSION="${WC_VERSION:-latest}" + +log() { + printf '[xwc-tests] %s\n' "$1" >&2 +} + +wp_cli() { + php -d memory_limit=512M "$(command -v wp)" "$@" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf 'Missing required command: %s\n' "$1" >&2 + exit 1 + fi +} + +wait_for_database() { + log "Waiting for MySQL at ${DB_HOST}:${DB_PORT}" + + for _ in $(seq 1 30); do + if mysqladmin ping \ + --protocol=tcp \ + --host="${DB_HOST}" \ + --port="${DB_PORT}" \ + --user="${DB_ROOT_USER}" \ + --password="${DB_ROOT_PASSWORD}" \ + --silent >/dev/null 2>&1; then + return 0 + fi + + sleep 2 + done + + printf 'Timed out waiting for MySQL.\n' >&2 + exit 1 +} + +ensure_database() { + mysql \ + --protocol=tcp \ + --host="${DB_HOST}" \ + --port="${DB_PORT}" \ + --user="${DB_ROOT_USER}" \ + --password="${DB_ROOT_PASSWORD}" \ + --execute="CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`;" +} + +ensure_wordpress() { + mkdir -p "${WP_CORE_DIR}" "${WP_DOWNLOADS_DIR}" + + if [ ! -f "${WP_CORE_DIR}/wp-load.php" ]; then + log "Downloading WordPress ${WP_VERSION}" + wp_cli core download --version="${WP_VERSION}" --path="${WP_CORE_DIR}" --force + fi + + if [ ! -f "${WP_CORE_DIR}/wp-config.php" ]; then + log 'Creating wp-config.php' + wp_cli config create \ + --path="${WP_CORE_DIR}" \ + --dbname="${DB_NAME}" \ + --dbuser="${DB_USER}" \ + --dbpass="${DB_PASSWORD}" \ + --dbhost="${DB_HOST}:${DB_PORT}" \ + --skip-check \ + --force + fi + + if ! wp_cli core is-installed --path="${WP_CORE_DIR}" >/dev/null 2>&1; then + log 'Installing WordPress site' + wp_cli core install \ + --path="${WP_CORE_DIR}" \ + --url="http://localhost:8080" \ + --title="XWC Data Type Tests" \ + --admin_user="admin" \ + --admin_password="password" \ + --admin_email="admin@example.org" + fi +} + +download_wordpress_develop() { + local wp_core_version="$1" + local archive_path="${WP_DOWNLOADS_DIR}/wordpress-develop-${wp_core_version}.zip" + local extract_root="${WP_DOWNLOADS_DIR}/wordpress-develop-${wp_core_version}" + local source_root + + if [ ! -d "${extract_root}" ]; then + mkdir -p "${WP_DOWNLOADS_DIR}" + + if [ ! -f "${archive_path}" ]; then + log "Downloading wordpress-develop ${wp_core_version}" + if ! curl -fsSL -o "${archive_path}" "https://github.com/WordPress/wordpress-develop/archive/refs/tags/${wp_core_version}.zip"; then + log "Falling back to trunk for wordpress-develop ${wp_core_version}" + curl -fsSL -o "${archive_path}" "https://github.com/WordPress/wordpress-develop/archive/refs/heads/trunk.zip" + fi + fi + + rm -rf "${extract_root}" + mkdir -p "${extract_root}" + unzip -q -o "${archive_path}" -d "${extract_root}" + fi + + source_root="$(find "${extract_root}" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + + if [ -z "${source_root}" ]; then + printf 'Unable to locate extracted wordpress-develop files.\n' >&2 + exit 1 + fi + + printf '%s\n' "${source_root}" +} + +ensure_wordpress_tests_suite() { + local wp_core_version source_root + + mkdir -p "${WP_TESTS_DIR}" + + if [ -f "${WP_TESTS_DIR}/includes/bootstrap.php" ] && [ -f "${WP_TESTS_DIR}/wp-tests-config.php" ]; then + return 0 + fi + + wp_core_version="$(wp_cli core version --path="${WP_CORE_DIR}")" + source_root="$(download_wordpress_develop "${wp_core_version}")" + + log "Preparing WordPress test suite for ${wp_core_version}" + rm -rf "${WP_TESTS_DIR}" + mkdir -p "${WP_TESTS_DIR}" + cp -R "${source_root}/tests/phpunit/." "${WP_TESTS_DIR}/" + + cat > "${WP_TESTS_DIR}/wp-tests-config.php" </dev/null 2>&1 || true +} + +main() { + require_command curl + require_command docker + require_command mysql + require_command mysqladmin + require_command php + require_command unzip + require_command wp + + export WP_CLI_CACHE_DIR="${CACHE_DIR}/wp-cli" + + wait_for_database + ensure_database + ensure_wordpress + ensure_wordpress_tests_suite + ensure_test_plugin + ensure_woocommerce + + log 'WordPress test environment is ready' +} + +main "$@" diff --git a/composer.json b/composer.json index 7fc7047..1c14cc2 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,13 @@ "x-wp/di": "^1.0 || ^2.0", "oblak/wordpress-coding-standard": "^1", "php-stubs/wordpress-stubs": "^6.5", + "phpunit/phpunit": "^9.6", "phpstan/extension-installer": "^1.3", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", "swissspidy/phpstan-no-private": "^0.2.0", - "szepeviktor/phpstan-wordpress": "^1.3" + "szepeviktor/phpstan-wordpress": "^1.3", + "yoast/phpunit-polyfills": "^4.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -60,5 +62,15 @@ "phpstan/extension-installer": true }, "platform-check": false + }, + "scripts": { + "test:env:start": "docker compose up -d mysql", + "test:env:stop": "docker compose down --remove-orphans", + "test:install": [ + "@test:env:start", + "bash bin/install-wp-tests.sh" + ], + "test": "vendor/bin/phpunit", + "test:clean": "bash bin/clean-wp-tests.sh" } } diff --git a/composer.lock b/composer.lock index 12b4091..0f88e95 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "169e0142ea35bc4d88c653fc5a108c4d", + "content-hash": "9c80341551de30f4febf11a40e3223d9", "packages": [ { "name": "automattic/jetpack-constants", @@ -826,6 +826,194 @@ ], "time": "2025-06-27T17:24:01+00:00" }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, { "name": "oblak/wordpress-coding-standard", "version": "v1.3.0", @@ -886,6 +1074,124 @@ ], "time": "2025-04-25T15:40:25+00:00" }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "php-stubs/wordpress-stubs", "version": "v6.8.1", @@ -1522,105 +1828,1546 @@ "time": "2024-09-11T15:52:35+00:00" }, { - "name": "slevomat/coding-standard", - "version": "8.19.1", + "name": "phpunit/php-code-coverage", + "version": "9.2.32", "source": { "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.13.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phing/phing": "3.0.1", - "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.17", - "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.6", - "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" + "phpunit/phpunit": "^9.6" }, - "type": "phpcodesniffer-standard", + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", "extra": { "branch-alias": { - "dev-master": "8.x-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { - "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "dev", - "phpcs" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { - "url": "https://github.com/kukulich", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", - "type": "tidelift" } ], - "time": "2025-06-09T17:53:57+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "name": "phpunit/php-file-iterator", + "version": "3.0.6", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^9.3" }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "3.0-dev" } }, - "notification-url": "https://packagist.org/downloads/", - "license": [ + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.34", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:45:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.19.1", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.1.0", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "require-dev": { + "phing/phing": "3.0.1", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.6", + "phpstan/phpstan-strict-rules": "2.0.4", + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2025-06-09T17:53:57+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ "BSD-3-Clause" ], "authors": [ @@ -1860,6 +3607,56 @@ }, "time": "2024-06-28T22:27:19+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.1.0", @@ -1925,16 +3722,79 @@ } ], "time": "2024-03-25T16:39:00+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/134921bfca9b02d8f374c48381451da1d98402f9", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0 || ^11.0 || ^12.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2025-02-09T18:58:54+00:00" } ], "aliases": [], "minimum-stability": "alpha", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.0" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bac6592 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mysql: + image: mysql:8.0 + environment: + MYSQL_DATABASE: wordpress_test + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: root + ports: + - "33067:3306" + healthcheck: + test: + - CMD-SHELL + - mysqladmin ping -h 127.0.0.1 -proot --silent + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..53eacbe --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + tests + + + + + + + + diff --git a/tests/SmokeTest.php b/tests/SmokeTest.php new file mode 100644 index 0000000..2b1e684 --- /dev/null +++ b/tests/SmokeTest.php @@ -0,0 +1,18 @@ +assertTrue(function_exists('add_action')); + $this->assertTrue(class_exists('WooCommerce')); + $this->assertTrue(class_exists(Entity_Manager::class)); + + $manager = Entity_Manager::instance(); + + $this->assertInstanceOf(Entity_Manager::class, $manager); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..98e1b13 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,47 @@ +wpdb_table_fix(); + WC_Install::create_tables(); + update_option('woocommerce_version', WC_VERSION); + update_option('woocommerce_db_version', WC()->db_version); + } + }, + 0 + ); + } +); + +require_once $_tests_dir . '/includes/bootstrap.php'; diff --git a/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php new file mode 100644 index 0000000..2d339cc --- /dev/null +++ b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php @@ -0,0 +1,9 @@ + Date: Sat, 4 Apr 2026 22:19:16 +0200 Subject: [PATCH 08/12] fix: improve robustness and add tests - Add null checks with RuntimeException in xwc_ds() and xwc_get_object_factory() - Fix meta store validation: require explicit store class when meta props defined - Fix term field normalization logic in Model decorator - Remove unused $keys array in Query_Handler::get_meta_query_args() - Fix XWC_Object_Query: return 0 instead of null for count, remove redundant reset(), fix orderby init - Add ModelDefinitionTest, QueryTest, and test support fixtures - Update wp-plugin test bootstrap to load test support files --- .codex | 0 src/Core/XWC_Object_Query.php | 10 +- src/Decorators/Model.php | 12 +- src/Repo/Query_Handler.php | 9 - src/Utils/xwc-data-utils-object.php | 16 +- tests/ModelDefinitionTest.php | 78 +++++++ tests/QueryTest.php | 192 ++++++++++++++++++ tests/Support/TestItem.php | 27 +++ tests/Support/fixtures.php | 70 +++++++ .../xwc-data-type-tests.php | 4 + 10 files changed, 399 insertions(+), 19 deletions(-) create mode 100644 .codex create mode 100644 tests/ModelDefinitionTest.php create mode 100644 tests/QueryTest.php create mode 100644 tests/Support/TestItem.php create mode 100644 tests/Support/fixtures.php diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/src/Core/XWC_Object_Query.php b/src/Core/XWC_Object_Query.php index 0f755d1..7cea3f8 100644 --- a/src/Core/XWC_Object_Query.php +++ b/src/Core/XWC_Object_Query.php @@ -100,10 +100,9 @@ public function count( ?array $query = null ): int { $query['fields'] = 'ids'; $this->query( $query ); - $this->reset(); } - return $this->total; + return $this->total ?? 0; } /** @@ -120,6 +119,7 @@ protected function parse( array $q ): void { 'order' => 'DESC', 'orderby' => $this->id_field, 'page' => 1, + 'per_page' => 20, ); $q = \wp_parse_args( $q, $d ); @@ -262,12 +262,10 @@ protected function init_terms( array &$c, array $q ): void { * @param array $q Query variables. */ protected function init_orderby( array &$c, array $q ): void { - $c['orderby'] = 'ORDER BY '; - $c['orderby'] = match ( $q['orderby'] ) { - 'rand' => 'RAND()', + 'rand' => 'RAND()', $this->id_field => "{$this->table}.{$this->id_field} {$q['order']}", - default => "{$this->table}.{$q['orderby']} {$q['order']}", + default => "{$this->table}.{$q['orderby']} {$q['order']}", }; } diff --git a/src/Decorators/Model.php b/src/Decorators/Model.php index cc26b9f..fd0c893 100644 --- a/src/Decorators/Model.php +++ b/src/Decorators/Model.php @@ -312,7 +312,11 @@ protected function set_meta_store( ?string $store ): ?string { return null; } - $store ??= XWC_Meta_Store::class; + if ( \is_null( $store ) ) { + throw new \InvalidArgumentException( + \esc_html( "A concrete meta store class must be provided when meta props are defined for '{$this->name}'." ), + ); + } if ( ! \class_exists( $store ) ) { throw new \InvalidArgumentException( \esc_html( "Meta store class $store does not exist." ) ); @@ -410,7 +414,11 @@ private function parse_tax_arg( array $args ): array { ), ); - $args['field'] = \preg_replace( '/^id$/', 'term_id', \ltrim( $args['field'], 'term_' ) ); + $field = $args['field']; + $field = \str_starts_with( $field, 'term_' ) ? \substr( $field, 5 ) : $field; + $field = 'id' === $field ? 'term_id' : $field; + + $args['field'] = $field; $args['default'] = 'array' === $args['return'] ? (array) $args['default'] : $args['default']; $args['type'] = \sprintf( 'term_%s|%s|%s', $args['return'], $args['field'], $args['taxonomy'] ); diff --git a/src/Repo/Query_Handler.php b/src/Repo/Query_Handler.php index e5135aa..d422b10 100644 --- a/src/Repo/Query_Handler.php +++ b/src/Repo/Query_Handler.php @@ -181,15 +181,6 @@ protected function get_date_query_args( array $vars, array $dates ): array { * @return array */ protected function get_meta_query_args( array $vars ): array { - $keys = array( - 'parent', - 'parent_exclude', - 'exclude', - 'limit', - 'type', - 'return', - ); - return parent::get_wp_query_args( $vars ); } diff --git a/src/Utils/xwc-data-utils-object.php b/src/Utils/xwc-data-utils-object.php index a453166..5c81a8c 100644 --- a/src/Utils/xwc-data-utils-object.php +++ b/src/Utils/xwc-data-utils-object.php @@ -14,8 +14,14 @@ * @return T */ function xwc_ds( string $name, string $cn = XWC_Data_Store_XT::class ): XWC_Data_Store_XT { + $entity = xwc_get_entity( $name ); + + if ( null === $entity ) { + throw new \RuntimeException( \esc_html( "Entity '{$name}' is not registered." ) ); + } + // @phpstan-ignore return.type - return xwc_get_entity( $name )->repo; + return $entity->repo; } /** @@ -35,7 +41,13 @@ function xwc_data_store( string $name ): WC_Data_Store { * @return XWC_Object_Factory */ function xwc_get_object_factory( string $name ): XWC_Object_Factory { - return xwc_get_entity( $name )->factory; + $entity = xwc_get_entity( $name ); + + if ( null === $entity ) { + throw new \RuntimeException( \esc_html( "Entity '{$name}' is not registered." ) ); + } + + return $entity->factory; } /** diff --git a/tests/ModelDefinitionTest.php b/tests/ModelDefinitionTest.php new file mode 100644 index 0000000..f71da86 --- /dev/null +++ b/tests/ModelDefinitionTest.php @@ -0,0 +1,78 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A concrete meta store class must be provided when meta props are defined'); + + new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array( + 'color' => array( + 'default' => '', + 'type' => 'string', + ), + ), + ); + } + + public function test_model_normalizes_tax_field_id_to_term_id(): void { + $model = new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array(), + array( + 'category' => array( + 'field' => 'id', + 'return' => 'single', + 'taxonomy' => 'category', + ), + ), + ); + + $this->assertSame('term_single|term_id|category', $model->tax_props['category']['type']); + $this->assertSame('term_id', $model->tax_props['category']['field']); + } + + public function test_model_keeps_term_id_tax_field_stable(): void { + $model = new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array(), + array( + 'category' => array( + 'field' => 'term_id', + 'return' => 'single', + 'taxonomy' => 'category', + ), + ), + ); + + $this->assertSame('term_single|term_id|category', $model->tax_props['category']['type']); + $this->assertSame('term_id', $model->tax_props['category']['field']); + } +} diff --git a/tests/QueryTest.php b/tests/QueryTest.php new file mode 100644 index 0000000..d6609b5 --- /dev/null +++ b/tests/QueryTest.php @@ -0,0 +1,192 @@ + 1, + 'name' => 'Alpha', + 'slug' => 'alpha', + 'score' => 10, + ), + array( + 'id' => 2, + 'name' => 'Beta', + 'slug' => 'beta', + 'score' => 20, + ), + array( + 'id' => 3, + 'name' => 'Gamma', + 'slug' => 'gamma', + 'score' => 30, + ), + ), + ); + + $query = new XWC_Object_Query( + xwc_test_custom_entity_table_name(), + 'id', + ); + + $this->assertSame( + 3, + $query->count( + array( + 'fields' => 'ids', + 'page' => 1, + 'per_page' => 2, + ), + ), + ); + $this->assertSame(3, $query->total); + } + + public function test_custom_query_can_filter_rows_without_paging(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 20 ), + ), + ); + + $query = new XWC_Object_Query( + xwc_test_custom_entity_table_name(), + 'id', + ); + + $this->assertSame( + array(2, 3), + $query->query( + array( + 'col_query' => array( + 'score' => 20, + ), + 'fields' => 'ids', + 'order' => 'ASC', + 'orderby' => 'id', + 'per_page' => 0, + ), + ), + ); + $this->assertSame(2, $query->total); + $this->assertSame(1, $query->pages); + } + + public function test_xwc_ds_returns_registered_repo_instance(): void { + $repo = xwc_ds(xwc_test_custom_entity_name()); + + $this->assertInstanceOf(XWC_Data_Store_XT::class, $repo); + $this->assertSame(xwc_test_custom_entity_table_name(), $repo->get_table()); + } + + public function test_repo_query_paginates_and_returns_ids(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 30 ), + ), + ); + + $results = xwc_ds(xwc_test_custom_entity_name())->query( + array( + 'limit' => 2, + 'order' => 'ASC', + 'orderby' => 'score', + 'paginate' => true, + 'return' => 'ids', + ), + ); + + $this->assertSame(2, $results['pages']); + $this->assertSame(3, $results['total']); + $this->assertCount(2, $results['objects']); + $this->assertSame(array(1, 2), $results['objects']); + } + + public function test_repo_query_can_return_hydrated_objects(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + ), + ); + + $results = xwc_ds(xwc_test_custom_entity_name())->query( + array( + 'order' => 'ASC', + 'orderby' => 'score', + 'return' => 'objects', + ), + ); + + $this->assertCount(2, $results); + $this->assertInstanceOf(XWC_Test_Item::class, $results[0]); + $this->assertSame('alpha', $results[0]->get_slug()); + $this->assertSame(20, $results[1]->get_score()); + } + + public function test_repo_count_applies_core_column_filters(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 20 ), + ), + ); + + $count = xwc_ds(xwc_test_custom_entity_name())->count( + array( + 'score' => 20, + ), + ); + + $this->assertSame(2, $count); + } + + public function test_repo_find_returns_first_matching_object(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + ), + ); + + $found = xwc_ds(xwc_test_custom_entity_name())->find( + array( + 'slug' => 'beta', + ), + ); + + $this->assertInstanceOf(XWC_Test_Item::class, $found); + $this->assertSame(2, $found->get_id()); + $this->assertSame('Beta', $found->get_name()); + } + + public function test_xwc_ds_throws_for_unknown_entities(): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('missing-entity'); + + xwc_ds('missing-entity'); + } + + public function test_xwc_get_object_factory_throws_for_unknown_entities(): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('missing-entity'); + + xwc_get_object_factory('missing-entity'); + } +} diff --git a/tests/Support/TestItem.php b/tests/Support/TestItem.php new file mode 100644 index 0000000..73c3db1 --- /dev/null +++ b/tests/Support/TestItem.php @@ -0,0 +1,27 @@ + array( + 'default' => '', + 'type' => 'string', + ), + 'slug' => array( + 'default' => '', + 'type' => 'slug', + ), + 'score' => array( + 'default' => 0, + 'type' => 'int', + ), + ), +)] +final class XWC_Test_Item extends XWC_Data { + protected $object_type = 'xwc_test_item'; +} diff --git a/tests/Support/fixtures.php b/tests/Support/fixtures.php new file mode 100644 index 0000000..565f48b --- /dev/null +++ b/tests/Support/fixtures.php @@ -0,0 +1,70 @@ +prefix . 'xwc_test_items'; +} + +function xwc_test_install_custom_entity(): void { + global $wpdb; + + if (! function_exists('dbDelta')) { + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + } + + $table = xwc_test_custom_entity_table_name(); + $charset = $wpdb->get_charset_collate(); + + dbDelta( + "CREATE TABLE {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL DEFAULT '', + slug VARCHAR(200) NOT NULL DEFAULT '', + score BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + KEY slug (slug), + KEY score (score) + ) {$charset};" + ); + + if (! xwc_entity_exists(xwc_test_custom_entity_name())) { + xwc_register_entity(XWC_Test_Item::class); + } +} + +function xwc_test_reset_custom_entity_table(): void { + global $wpdb; + + xwc_test_install_custom_entity(); + $wpdb->query('TRUNCATE TABLE ' . xwc_test_custom_entity_table_name()); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared +} + +/** + * @param array $rows + */ +function xwc_test_seed_custom_entity_rows(array $rows): void { + global $wpdb; + + xwc_test_install_custom_entity(); + + foreach ($rows as $row) { + $wpdb->insert( + xwc_test_custom_entity_table_name(), + wp_parse_args( + $row, + array( + 'name' => '', + 'score' => 0, + 'slug' => '', + ) + ) + ); + } +} diff --git a/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php index 2d339cc..ab4732b 100644 --- a/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php +++ b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php @@ -7,3 +7,7 @@ declare(strict_types=1); require_once dirname(__DIR__, 3) . '/vendor/autoload.php'; +require_once dirname(__DIR__, 2) . '/Support/TestItem.php'; +require_once dirname(__DIR__, 2) . '/Support/fixtures.php'; + +xwc_test_install_custom_entity(); From 26e6c81698f84544eb825362af7fe42a0d9b3580 Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 22:22:21 +0200 Subject: [PATCH 09/12] test: add data object and prop coverage --- tests/DataObjectTest.php | 109 ++++++++++++++++++++++++++++++++++++ tests/ObjectFactoryTest.php | 82 +++++++++++++++++++++++++++ tests/PropTest.php | 75 +++++++++++++++++++++++++ tests/Support/TestProp.php | 12 ++++ 4 files changed, 278 insertions(+) create mode 100644 tests/DataObjectTest.php create mode 100644 tests/ObjectFactoryTest.php create mode 100644 tests/PropTest.php create mode 100644 tests/Support/TestProp.php diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php new file mode 100644 index 0000000..982dfa3 --- /dev/null +++ b/tests/DataObjectTest.php @@ -0,0 +1,109 @@ +set_name('Alpha'); + $item->set_slug('alpha-item'); + $item->set_score('42'); + + $this->assertSame('Alpha', $item->get_name()); + $this->assertSame('alpha-item', $item->get_slug()); + $this->assertSame(42, $item->get_score()); + } + + public function test_hydrated_item_reports_core_data_and_core_changes(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $item = new XWC_Test_Item(1); + $item->set_score(15); + + $this->assertSame( + array( + 'name' => 'Alpha', + 'slug' => 'alpha', + 'score' => 15, + 'id' => 1, + ), + $item->get_core_data('view', true), + ); + $this->assertSame( + array( + 'score' => 15, + ), + $item->get_core_changes(), + ); + } + + public function test_json_serialize_returns_object_data_without_meta_data_key(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $item = new XWC_Test_Item(1); + $data = $item->jsonSerialize(); + + $this->assertSame(1, $data['id']); + $this->assertSame('Alpha', $data['name']); + $this->assertArrayNotHasKey('meta_data', $data); + } + + public function test_serialization_round_trip_restores_object_identity(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + /** @var XWC_Test_Item $restored */ + $restored = unserialize(serialize(new XWC_Test_Item(1))); + + $this->assertInstanceOf(XWC_Test_Item::class, $restored); + $this->assertSame(1, $restored->get_id()); + $this->assertSame('alpha', $restored->get_slug()); + } + + public function test_object_helpers_return_defaults_and_paged_results(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 30 ), + ), + ); + + $missing = xwc_get_object(999, xwc_test_custom_entity_name(), null); + $paged = xwc_get_objects( + xwc_test_custom_entity_name(), + array( + 'limit' => 2, + 'order' => 'ASC', + 'orderby' => 'score', + 'paginate' => true, + 'return' => 'ids', + ), + ); + + $this->assertNull($missing); + $this->assertSame(array(1, 2), $paged['objects']); + $this->assertSame(2, $paged['pages']); + $this->assertSame(3, $paged['total']); + } +} diff --git a/tests/ObjectFactoryTest.php b/tests/ObjectFactoryTest.php new file mode 100644 index 0000000..ae262e3 --- /dev/null +++ b/tests/ObjectFactoryTest.php @@ -0,0 +1,82 @@ +assertSame(25, $factory->get_id(25)); + $this->assertSame(25, $factory->get_id('25')); + } + + public function test_factory_get_id_accepts_data_objects_and_globals(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + $object = new XWC_Test_Item(1); + + $GLOBALS[xwc_test_custom_entity_name()] = $object; + + $this->assertSame(1, $factory->get_id($object)); + $this->assertSame(1, $factory->get_id(null)); + } + + public function test_factory_get_object_returns_null_for_missing_ids(): void { + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + + $this->assertNull($factory->get_object(false)); + } + + public function test_factory_make_object_returns_concrete_object_even_for_zero_id(): void { + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + $object = $factory->make_object(0); + + $this->assertInstanceOf(XWC_Test_Item::class, $object); + $this->assertSame(0, $object->get_id()); + } + + public function test_object_classname_falls_back_to_base_class_when_filter_returns_invalid_class(): void { + add_filter( + 'xwc_' . xwc_test_custom_entity_name() . '_class', + static fn (): string => 'Missing_Class', + ); + + try { + $this->assertSame(XWC_Data::class, xwc_get_object_classname(1, xwc_test_custom_entity_name())); + } finally { + remove_all_filters('xwc_' . xwc_test_custom_entity_name() . '_class'); + } + } + + public function test_get_object_instance_returns_hydrated_object(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $object = xwc_get_object_instance(1, xwc_test_custom_entity_name()); + + $this->assertInstanceOf(XWC_Test_Item::class, $object); + $this->assertSame('alpha', $object->get_slug()); + } +} diff --git a/tests/PropTest.php b/tests/PropTest.php new file mode 100644 index 0000000..b64d88a --- /dev/null +++ b/tests/PropTest.php @@ -0,0 +1,75 @@ +assertNull($prop->jsonSerialize()); + } + + public function test_set_marks_prop_as_changed_after_read(): void { + $prop = new XWC_Test_Prop(); + + $prop->set('alpha', 'b'); + + $this->assertTrue($prop->changed()); + $this->assertSame('b', $prop->get('alpha')); + } + + public function test_set_data_marks_prop_as_changed_after_read(): void { + $prop = new XWC_Test_Prop(); + + $prop->set_data( + array( + 'alpha' => 'z', + 'items' => array(1, 2, 3), + ), + ); + + $this->assertTrue($prop->changed()); + } + + public function test_with_data_clones_from_other_prop_instances(): void { + $source = new XWC_Test_Prop( + array( + 'alpha' => 'source', + 'items' => array(4, 5), + ), + ); + $clone = XWC_Test_Prop::default()->with_data($source); + + $this->assertInstanceOf(XWC_Test_Prop::class, $clone); + $this->assertNotSame($source, $clone); + $this->assertSame($source->get_data(), $clone->get_data()); + } + + public function test_serialization_round_trip_preserves_data(): void { + $prop = new XWC_Test_Prop( + array( + 'alpha' => 'serialized', + 'items' => array(9, 7), + ), + ); + + /** @var XWC_Test_Prop $roundTrip */ + $roundTrip = unserialize(serialize($prop)); + + $this->assertInstanceOf(XWC_Test_Prop::class, $roundTrip); + $this->assertSame($prop->get_data(), $roundTrip->get_data()); + } + + public function test_array_access_mutators_throw(): void { + $prop = new XWC_Test_Prop(); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Do not use this method directly.'); + + $prop['alpha'] = 'c'; + } +} diff --git a/tests/Support/TestProp.php b/tests/Support/TestProp.php new file mode 100644 index 0000000..2706c1d --- /dev/null +++ b/tests/Support/TestProp.php @@ -0,0 +1,12 @@ + 'a', + 'items' => array(3, 1, 2), + ); + } +} From a24713932348ed8d064e0242ac6f6042b1b8f16f Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 22:50:52 +0200 Subject: [PATCH 10/12] fix: include all runtime files in release artifact --- .releaserc | 2 +- tests/ReleaseConfigTest.php | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/ReleaseConfigTest.php diff --git a/.releaserc b/.releaserc index 9053ecc..c09df5f 100644 --- a/.releaserc +++ b/.releaserc @@ -20,7 +20,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "zip -r '/tmp/release.zip' ./src README.md" + "prepareCmd": "zip -r '/tmp/release.zip' src lib composer.json README.md LICENSE" } ], [ diff --git a/tests/ReleaseConfigTest.php b/tests/ReleaseConfigTest.php new file mode 100644 index 0000000..caaad8f --- /dev/null +++ b/tests/ReleaseConfigTest.php @@ -0,0 +1,41 @@ +assertIsArray($exec_plugin); + $this->assertArrayHasKey('prepareCmd', $exec_plugin); + $this->assertStringContainsString('src', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('lib', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('composer.json', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('README.md', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('LICENSE', $exec_plugin['prepareCmd']); + } + + public function test_release_config_keeps_beta_as_prerelease_branch(): void { + $config = json_decode((string) file_get_contents(dirname(__DIR__) . '/.releaserc'), true, 512, JSON_THROW_ON_ERROR); + + $this->assertSame('master', $config['branches'][0]); + $this->assertContains( + array( + 'name' => 'beta', + 'prerelease' => true, + ), + $config['branches'] + ); + } +} From 1b767438e7e57a1ecc6ce07b74e4106fa33c88dd Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 22:50:55 +0200 Subject: [PATCH 11/12] docs: add release process guide and rewrite README --- README.md | 124 +++++++++++++++++++++++++++++++++++++++- docs/release-process.md | 26 +++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 docs/release-process.md diff --git a/README.md b/README.md index 6bfdefe..e3bb658 100644 --- a/README.md +++ b/README.md @@ -1 +1,123 @@ -# WC Data eXtender +
+ +

WC Data Type

+

Model-driven custom data objects for WooCommerce

+ +[![Packagist Version](https://img.shields.io/packagist/v/x-wp/wc-data-type?label=Release&style=flat-square)](https://packagist.org/packages/x-wp/wc-data-type) +![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/x-wp/wc-data-type/php?label=PHP&logo=php&logoColor=white&logoSize=auto&style=flat-square) +![Static Badge](https://img.shields.io/badge/WP-%3E%3D6.9.4-3858e9?style=flat-square&logo=wordpress&logoSize=auto) +![Static Badge](https://img.shields.io/badge/WC-%3E%3D10.6.1-7f54b3?style=flat-square&logo=woocommerce&logoSize=auto) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/x-wp/wc-data-type/tests.yml?label=Tests&event=push&style=flat-square&logo=githubactions&logoColor=white&logoSize=auto)](https://github.com/x-wp/wc-data-type/actions/workflows/tests.yml) + +
+ +This library provides a standardized way to define and work with custom WooCommerce-style data objects. Define a model once, then reuse consistent property handling, factories, queries, and data-store behavior around it. + +## Key Features + +1. Model-driven setup: Define object shape with the `#[Model]` attribute. +2. WooCommerce-style objects: Extend `XWC_Data` and keep a familiar CRUD workflow. +3. Typed properties: Support for core props, meta props, and taxonomy props. +4. Shared object helpers: Load single objects or collections through package utilities. +5. Extensible architecture: Swap in custom data stores, factories, and meta stores when needed. +6. WordPress-native integration: Designed for plugin code already built around WordPress and WooCommerce lifecycles. + +## Installation + +You can install this package via Composer: + +```bash +composer require x-wp/wc-data-type +``` + +> [!TIP] +> We recommend using `automattic/jetpack-autoloader` with this package to reduce autoloading conflicts in WordPress environments. + +## Usage + +Below is a simple example that defines a custom data object and loads it through the package helpers. + +### Defining a model + +```php + array( + 'default' => '', + 'type' => 'string', + ), + 'slug' => array( + 'default' => '', + 'type' => 'slug', + ), + 'price' => array( + 'default' => 0, + 'type' => 'float', + ), + ), +)] +final class Book extends XWC_Data { + protected $object_type = 'book'; +} +``` + +### Loading objects + +```php + 10, + 'orderby' => 'title', + 'order' => 'ASC', + ), +); +``` + +Generated getters and setters follow the declared props, so classes like `Book` can expose methods such as `get_title()`, `set_title()`, `get_slug()`, and `set_price()`. + +## Testing + +The package ships with a PHPUnit suite that boots a WordPress test environment and exercises model definitions, object factories, queries, props, and runtime object behavior. + +Run the suite with: + +```bash +composer test +``` + +To prepare the local WordPress test environment first: + +```bash +composer test:install +composer test +``` + +To clean the local test environment: + +```bash +composer test:clean +``` + +## Documentation + +For package-specific usage, start with the public entrypoints used throughout the library: + +- `XWC\Data\Decorators\Model` +- `XWC_Data` +- `xwc_get_object()` +- `xwc_get_objects()` + +Additional project information is available in the [repository](https://github.com/x-wp/wc-data-type). + +For maintainers preparing prereleases or stable tags, see [docs/release-process.md](docs/release-process.md). diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..edfa798 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,26 @@ +# Release Process + +## Branches + +- `beta` is the prerelease branch. Pushes here may publish prerelease builds only. +- `master` is the stable release branch. Stable tags are cut from commits that land here. + +## v2.0.0 Trigger + +Semantic Release will not infer a major version from test, fix, refactor, or chore commits alone. To cut `v2.0.0`, the merge commit that lands on `master` must include an explicit breaking-change signal: + +- Use a conventional commit subject with `!`, such as `feat!: ship stateful data API`. +- Or include a `BREAKING CHANGE:` footer in the commit body that explains the incompatible change. + +## Release Checklist + +Before merging a stable release to `master`: + +1. Run `composer test` and confirm the full suite passes. +2. Confirm the semantic-release dry-run job passes in GitHub Actions. +3. Verify the release artifact contains `src/`, `lib/`, `composer.json`, `README.md`, and `LICENSE`. +4. Summarize the breaking changes and migration notes in the release notes. + +## Notes + +The GitHub release artifact is intended to be usable outside a Packagist install, so it must include both the runtime source tree and the autoload metadata declared in `composer.json`. From 6d9fa56d2992463ee12d3bece850257d996c509c Mon Sep 17 00:00:00 2001 From: Sibin Grasic Date: Sat, 4 Apr 2026 22:50:58 +0200 Subject: [PATCH 12/12] ci: add semantic-release dry-run job to test workflow --- .github/workflows/tests.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4aa7549..8553979 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,3 +45,22 @@ jobs: - name: Run PHPUnit run: composer test + + semantic-release-dry-run: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Semantic Release Dry Run + uses: cycjimmy/semantic-release-action@v4 + with: + dry_run: true + extra_plugins: | + @semantic-release/github + @semantic-release/exec + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}