diff --git a/composer.lock b/composer.lock index 61daa148..efaa4303 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": "2c87ae59285b238b9fec5f37c0e81c13", + "content-hash": "861c364aee35aaed117a571519c45d72", "packages": [ { "name": "firebase/php-jwt", @@ -1259,47 +1259,52 @@ }, { "name": "symfony/console", - "version": "v6.4.13", + "version": "v5.4.46", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f793dd5a7d9ae9923e35d0503d08ba734cec1d79" + "reference": "fb0d4760e7147d81ab4d9e2d57d56268261b4e4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f793dd5a7d9ae9923e35d0503d08ba734cec1d79", - "reference": "f793dd5a7d9ae9923e35d0503d08ba734cec1d79", + "url": "https://api.github.com/repos/symfony/console/zipball/fb0d4760e7147d81ab4d9e2d57d56268261b4e4e", + "reference": "fb0d4760e7147d81ab4d9e2d57d56268261b4e4e", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" }, "type": "library", "autoload": { @@ -1333,7 +1338,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.13" + "source": "https://github.com/symfony/console/tree/v5.4.46" }, "funding": [ { @@ -1349,7 +1354,7 @@ "type": "tidelift" } ], - "time": "2024-10-09T08:40:40+00:00" + "time": "2024-11-05T14:17:06+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1812,6 +1817,86 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.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": "symfony/service-contracts", "version": "v3.5.0", diff --git a/includes/connection/class-product-attributes.php b/includes/connection/class-product-attributes.php index d7f89a7e..7b4b662e 100644 --- a/includes/connection/class-product-attributes.php +++ b/includes/connection/class-product-attributes.php @@ -48,6 +48,23 @@ public static function register_connections() { ] ) ); + + // From RootQuery to GlobalProductAttribute. + register_graphql_connection( + self::get_connection_config( + [ + 'fromType' => 'RootQuery', + 'toType' => 'GlobalProductAttribute', + 'fromFieldName' => 'productAttributes', + 'connectionArgs' => [], + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Product_Attribute_Connection_Resolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ] + ) + ); } /** @@ -73,16 +90,21 @@ public static function get_connection_config( $args = [] ): array { 'fromFieldName' => 'attributes', 'connectionArgs' => [], 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { - $resolver = new Product_Attribute_Connection_Resolver(); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase switch ( $info->fieldName ) { case 'globalAttributes': - return $resolver->resolve( $source, $args, $context, $info, 'global' ); + $resolver = new Product_Attribute_Connection_Resolver( $source, $args, $context, $info, 'global' ); + break; case 'localAttributes': - return $resolver->resolve( $source, $args, $context, $info, 'local' ); + $resolver = new Product_Attribute_Connection_Resolver( $source, $args, $context, $info, 'local' ); + break; default: - return $resolver->resolve( $source, $args, $context, $info ); + $resolver = new Product_Attribute_Connection_Resolver( $source, $args, $context, $info ); + break; } + + return $resolver->get_connection(); }, ], $args diff --git a/includes/data/connection/class-product-attribute-connection-resolver.php b/includes/data/connection/class-product-attribute-connection-resolver.php index 5b3af9b8..0bc01ec0 100644 --- a/includes/data/connection/class-product-attribute-connection-resolver.php +++ b/includes/data/connection/class-product-attribute-connection-resolver.php @@ -21,6 +21,59 @@ * Class Product_Attribute_Connection_Resolver */ class Product_Attribute_Connection_Resolver { + + /** + * The source from the field calling the connection. + * + * @var \WPGraphQL\WooCommerce\Model\Product|null + */ + protected $source; + + /** + * The args input on the field calling the connection. + * + * @var ?array + */ + protected $args; + + /** + * The AppContext for the GraphQL Request + * + * @var \WPGraphQL\AppContext + */ + protected $context; + + /** + * The ResolveInfo for the GraphQL Request + * + * @var \GraphQL\Type\Definition\ResolveInfo + */ + protected $info; + + /** + * The attribute type. + * + * @var string + */ + protected $type; + + /** + * Product_Attribute_Connection_Resolver constructor. + * + * @param \WPGraphQL\WooCommerce\Model\Product|null $source Source node. + * @param ?array $args Connection arguments. + * @param \WPGraphQL\AppContext $context AppContext object. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo object. + * @param string $type Attribute type. + */ + public function __construct( $source, array $args, AppContext $context, ResolveInfo $info, string $type = null ) { + $this->source = $source; + $this->args = $args; + $this->context = $context; + $this->info = $info; + $this->type = $type; + } + /** * Builds Product attribute items * @@ -33,25 +86,82 @@ class Product_Attribute_Connection_Resolver { * * @throws \GraphQL\Error\UserError Invalid product attribute enumeration value. * @return array + * + * @deprecated TBD */ private function get_items( $attributes, $source, $args, $context, $info, $type = null ) { + _deprecated_function( __METHOD__, 'TBD', static::class . '::build_nodes_from_product_attributes()' ); + + $this->source = $source; + $this->args = $args; + $this->context = $context; + $this->info = $info; + $this->type = $type; + + return $this->build_nodes_from_source_attributes(); + } + + /** + * Creates connection + * + * @param mixed $source Connection source Model instance. + * @param array $args Connection arguments. + * @param \WPGraphQL\AppContext $context AppContext object. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo object. + * @param string $type Attribute type. + * + * @return array|null + * + * @deprecated TBD + */ + public function resolve( $source, array $args, AppContext $context, ResolveInfo $info, $type = null ) { + _deprecated_function( __METHOD__, 'TBD', static::class . '::get_connection()' ); + + $this->source = $source; + $this->args = $args; + $this->context = $context; + $this->info = $info; + $this->type = $type; + + return $this->get_connection(); + } + + /** + * Builds connection nodes from source product's attributes. + * + * @return array + */ + private function build_nodes_from_source_attributes() { $items = []; + if ( ! $this->source ) { + return $items; + } + + $attributes = $this->source->attributes; + + + if ( empty( $attributes ) ) { + return $items; + } + foreach ( $attributes as $attribute_name => $data ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $data->_relay_id = base64_encode( $attribute_name . GLOBAL_ID_DELIMITER - . $source->ID + . $this->source->ID . GLOBAL_ID_DELIMITER . $data->get_name() ); $items[] = $data; } - $type = ! empty( $args['where']['type'] ) ? $args['where']['type'] : $type; + $attribute_type = ! empty( $this->args['where'] ) && ! empty( $this->args['where']['type'] ) + ? $this->args['where']['type'] + : $this->type; - if ( ! is_null( $type ) ) { - switch ( $type ) { + if ( ! is_null( $attribute_type ) ) { + switch ( $attribute_type ) { case 'local': $items = array_filter( $items, @@ -77,20 +187,25 @@ static function ( $item ) { } /** - * Creates connection + * Builds connection nodes from woocommerce global product attributes. * - * @param mixed $source Connection source Model instance. - * @param array $args Connection arguments. - * @param \WPGraphQL\AppContext $context AppContext object. - * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo object. - * @param string $type Attribute type. + * @return array + */ + private function build_nodes_from_global_attributes() { + // TODO: Implement this method. + return []; + } + + /** + * Builds connection from nodes array. + * + * @param array $nodes Array of connection nodes. * * @return array|null */ - public function resolve( $source, array $args, AppContext $context, ResolveInfo $info, $type = null ) { - $attributes = $this->get_items( $source->attributes, $source, $args, $context, $info, $type ); - - $connection = Relay::connectionFromArray( $attributes, $args ); + private function build_connection( $nodes = [] ) { + $connection = $this->build_connection( $nodes ); + $connection = Relay::connectionFromArray( $nodes, $this->args ); $nodes = []; if ( ! empty( $connection['edges'] ) && is_array( $connection['edges'] ) ) { foreach ( $connection['edges'] as $edge ) { @@ -100,4 +215,19 @@ public function resolve( $source, array $args, AppContext $context, ResolveInfo $connection['nodes'] = ! empty( $nodes ) ? $nodes : null; return ! empty( $attributes ) ? $connection : null; } + + /** + * Constructs the connection. + * + * @return array|null + */ + public function get_connection() { + if ( ! $this->source ) { + $attributes = $this->build_nodes_from_global_attributes(); + } else { + $attributes = $this->build_nodes_from_source_attributes(); + } + + return $this->build_connection( $attributes ); + } } diff --git a/includes/type/interface/class-product-with-attributes.php b/includes/type/interface/class-product-with-attributes.php index b8139889..6d4f6b68 100644 --- a/includes/type/interface/class-product-with-attributes.php +++ b/includes/type/interface/class-product-with-attributes.php @@ -81,9 +81,8 @@ public static function get_connections() { ], ], 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { - $resolver = new Product_Attribute_Connection_Resolver(); - - return $resolver->resolve( $source, $args, $context, $info ); + $resolver = new Product_Attribute_Connection_Resolver( $source, $args, $context, $info ); + return $resolver->get_connection(); }, ], ]; diff --git a/tests/wpunit/ProductAttributeQueriesTest.php b/tests/wpunit/ProductAttributeQueriesTest.php index acd201ca..f940a0f1 100644 --- a/tests/wpunit/ProductAttributeQueriesTest.php +++ b/tests/wpunit/ProductAttributeQueriesTest.php @@ -235,4 +235,59 @@ public function testProductAttributeMatchesVariationAttributeCounterpart() { [ $this->expectedField( 'product.id', $this->toRelayId( 'post', $product_id ) ) ] ); } + + public function testProductAttributesQuery() { + $this->factory->product->createAttribute( 'texture', [ 'smooth', 'rough', 'tiled' ] ); + $this->factory->product->createAttribute( 'tile-size', [ 'small', 'medium', 'large' ] ); + + $query = ' + query { + productAttributes { + nodes { + id + attributeId + name + label + options + position + visible + variation + } + } + } + '; + + $response = $this->graphql( compact( 'query' ) ); + $expected = [ + $this->expectedNode( + 'productAttributes.nodes', + [ + $this->expectedField( 'id', base64_encode( 'pattern' ) ), + $this->expectedField( 'name', 'pattern' ), + $this->expectedField( 'label', 'Pattern' ), + $this->expectedField( 'options', [ 'polka-dot', 'stripe', 'flames' ] ), + ] + ), + $this->expectedNode( + 'productAttributes.nodes', + [ + $this->expectedField( 'id', base64_encode( 'texture' ) ), + $this->expectedField( 'name', 'texture' ), + $this->expectedField( 'label', 'Texture' ), + $this->expectedField( 'options', [ 'smooth', 'rough', 'tiled' ] ), + ] + ), + $this->expectedNode( + 'productAttributes.nodes', + [ + $this->expectedField( 'id', base64_encode( 'tile-size' ) ), + $this->expectedField( 'name', 'tile-size' ), + $this->expectedField( 'label', 'Tile Size' ), + $this->expectedField( 'options', [ 'small', 'medium', 'large' ] ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } }