From 4d6e4e68b5d64ffae768705a761552201f9cae3e Mon Sep 17 00:00:00 2001 From: Matheus Paiva Date: Mon, 23 Dec 2019 20:53:13 +0000 Subject: [PATCH] First commit --- .gitignore | 1 + composer.json | 27 ++++ composer.lock | 302 +++++++++++++++++++++++++++++++++++++++++++ src/Container.php | 187 +++++++++++++++++++++++++++ src/Field.php | 192 +++++++++++++++++++++++++++ src/MetaResolver.php | 81 ++++++++++++ 6 files changed, 790 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Container.php create mode 100644 src/Field.php create mode 100644 src/MetaResolver.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a725465 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..edb72fb --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "mpaiva/wp-graphql-crb", + "description": "Wordpress wrapper to expose Carbon Fields to WpGraphQL queries", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Matheus Paiva", + "email": "mmccpp@gmail.com" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:matepaiva/carbon-fields.git" + } + ], + "autoload": { + "psr-4": { + "WpGraphQLCrb\\": "src/" + } + }, + "require": { + "htmlburger/carbon-fields": "dev-exopse_registered_fields", + "wp-graphql/wp-graphql": "^0.5.1" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..84ad7ea --- /dev/null +++ b/composer.lock @@ -0,0 +1,302 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "55a97d87f6adf6fe35b8f945d4503fd2", + "packages": [ + { + "name": "htmlburger/carbon-fields", + "version": "dev-exopse_registered_fields", + "source": { + "type": "git", + "url": "https://github.com/matepaiva/carbon-fields.git", + "reference": "401988372806c54f4b0af6ec937113e88e77c68b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matepaiva/carbon-fields/zipball/401988372806c54f4b0af6ec937113e88e77c68b", + "reference": "401988372806c54f4b0af6ec937113e88e77c68b", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "mockery/mockery": "^0.9.7", + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon_Fields\\": "core/" + } + }, + "license": [ + "GPL-2.0-only" + ], + "authors": [ + { + "name": "htmlBurger", + "email": "wordpress@htmlburger.com", + "homepage": "https://htmlburger.com/", + "role": "Developer" + }, + { + "name": "Miroslav Mitev", + "email": "mmitev.2create@gmail.com", + "role": "Developer" + }, + { + "name": "Atanas Angelov", + "email": "atanas.angelov.dev@gmail.com", + "role": "Developer" + }, + { + "name": "Georgi Stoyanov", + "email": "stoyanov.gs@gmail.com", + "role": "Developer" + }, + { + "name": "Plamen Kostadinov", + "email": "pkostadinov.2create@gmail.com", + "homepage": "http://plasmen.info/", + "role": "Developer" + }, + { + "name": "Stanimir Panchev", + "email": "Stan4omir@gmail.com", + "role": "Developer" + }, + { + "name": "Marin Atanasov", + "email": "contact@marinatanasov.com", + "homepage": "http://marinatanasov.com/", + "role": "Developer" + }, + { + "name": "Siyan Panayotov", + "homepage": "http://siyanpanayotov.com/", + "role": "Developer" + }, + { + "name": "Peter Petrov", + "email": "peter.petrov89@gmail.com", + "role": "Developer" + }, + { + "name": "Stanimir Stoyanov", + "email": "stanimir.k.stoyanov@gmail.com", + "role": "Developer" + }, + { + "name": "Kaloyan Ivanov", + "email": "kaloyanxivanov@gmail.com", + "homepage": "http://vilepixels.com/", + "role": "Developer" + }, + { + "name": "Georgi Popov", + "homepage": "http://magadanski.com/", + "role": "Developer" + }, + { + "name": "German Velchev", + "email": "germozy@gmail.com", + "role": "Developer" + }, + { + "name": "Rashko Petrov", + "email": "brutalenemy666@gmail.com", + "homepage": "http://errorfactory.com/", + "role": "Developer" + }, + { + "name": "Alexander Panayotov", + "email": "alexander.panayotov@gmail.com", + "homepage": "http://alexanderpanayotov.com/", + "role": "Developer" + }, + { + "name": "Viktor Vasilev", + "email": "liberalcho@gmail.com", + "role": "Developer" + }, + { + "name": "Georgi Georgiev", + "email": "george.georgiev96@gmail.com", + "role": "Developer" + }, + { + "name": "Atanas Vasilev", + "email": "atanasvasilev91@gmail.com", + "role": "Developer" + } + ], + "description": "WordPress developer-friendly custom fields for post types, taxonomy terms, users, comments, widgets, options and more.", + "homepage": "http://carbonfields.net/", + "support": { + "source": "https://github.com/htmlburger/carbon-fields", + "issues": "https://github.com/htmlburger/carbon-fields/issues", + "docs": "http://carbonfields.net/docs/", + "email": "wordpress@htmlburger.com" + }, + "time": "2019-12-23T11:08:25+00:00" + }, + { + "name": "ivome/graphql-relay-php", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ivome/graphql-relay-php.git", + "reference": "bc5f8aae9fd72ca16decce3892ec4311e9742a93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ivome/graphql-relay-php/zipball/bc5f8aae9fd72ca16decce3892ec4311e9742a93", + "reference": "bc5f8aae9fd72ca16decce3892ec4311e9742a93", + "shasum": "" + }, + "require": { + "php": ">=5.4,<8.0-DEV", + "webonyx/graphql-php": ">=0.7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "~1.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "description": "A PHP port of GraphQL Relay reference implementation", + "homepage": "https://github.com/ivome/graphql-relay-php", + "keywords": [ + "Relay", + "api", + "graphql" + ], + "time": "2016-10-29T23:29:29+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v0.12.6", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^4.8", + "psr/http-message": "^1.0", + "react/promise": "2.*" + }, + "suggest": { + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "time": "2018-09-02T14:59:54+00:00" + }, + { + "name": "wp-graphql/wp-graphql", + "version": "v0.5.1", + "source": { + "type": "git", + "url": "https://github.com/wp-graphql/wp-graphql.git", + "reference": "c6fbc3bdbce610cdc5996cd2023b37606d09de0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-graphql/wp-graphql/zipball/c6fbc3bdbce610cdc5996cd2023b37606d09de0c", + "reference": "c6fbc3bdbce610cdc5996cd2023b37606d09de0c", + "shasum": "" + }, + "require": { + "ivome/graphql-relay-php": "^0.3.1", + "webonyx/graphql-php": "0.12.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "lucatume/wp-browser": "^2.2.", + "phpcompatibility/phpcompatibility-wp": "^2.0", + "wp-coding-standards/wpcs": "^2.1" + }, + "type": "wordpress-plugin", + "autoload": { + "psr-4": { + "WPGraphQL\\": "src/" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Jason Bahl", + "email": "jasonbahl@mac.com" + }, + { + "name": "Edwin Cromley" + }, + { + "name": "Ryan Kanner" + }, + { + "name": "Hughie Devore" + }, + { + "name": "Chris Zarate" + } + ], + "description": "GraphQL API for WordPress", + "time": "2019-12-13T17:18:57+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "htmlburger/carbon-fields": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..8e5b369 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,187 @@ +container = $container; + $this->fields = $this->getFields(); + \add_action('graphql_register_types', [$this, 'graphqlRegisterTypes']); + } + + static public function register(CrbContainer $container) + { + return new Self($container); + } + + private function registerField(Field $field) + { + $roots = $this->getGraphQLRoot(); + $field_name = $field->getBaseName(); + $options = [ + 'type' => $field->getType($field), + 'description' => $field->getDescription($field), + 'resolve' => $this->getGraphQLResolver($field), + ]; + + foreach ($roots as $root) { + register_graphql_field($root, $field_name, $options); + } + } + + public function graphqlRegisterTypes() + { + Container::registerStaticObjectTypes(); + Container::$is_first_time = false; + $this->registerFields(); + } + + private function registerFields() + { + foreach ($this->fields as $field) { + $this->registerField($field); + } + } + + static function registerStaticObjectTypes() + { + if (Container::$is_first_time) { + register_graphql_object_type('Crb_Select', [ + 'description' => \__("The selected option/radio", 'app'), + 'fields' => [ + 'label' => [ + 'type' => 'String', + 'description' => \__('The label of the option', 'app'), + ], + 'value' => [ + 'type' => 'String', + 'description' => \__('The value of the option', 'app'), + ], + 'id' => [ + 'type' => 'String', + 'description' => \__('The value of the option', 'app'), + ], + ], + ]); + } + } + + private function getGraphQLResolver(Field $field) + { + $resolver = $this->getResolver($field); + + $field_resolver = $field->getResolver(); + + return $resolver($field_resolver); + } + + private function getResolver(Field $field) + { + return function ($cb) use ($field) { + switch ($this->container->type) { + case 'post_meta': + return function (Post $post, $args, AppContext $context, ResolveInfo $info) use ($field, $cb) { + $value = carbon_get_post_meta($post->ID, $field->getBaseName()); + return $cb($value, $field, $this->container, $args, $context, $info); + }; + + case 'term_meta': + return function (Term $term, $args, AppContext $context, ResolveInfo $info) use ($field, $cb) { + $value = carbon_get_term_meta($term->term_id, $field->getBaseName()); + return $cb($value, $field, $this->container, $args, $context, $info); + }; + + case 'user_meta': + return function (User $user, $args, AppContext $context, ResolveInfo $info) use ($field, $cb) { + $value = carbon_get_user_meta($user->userId, $field->getBaseName()); + return $cb($value, $field, $this->container, $args, $context, $info); + }; + + case 'theme_options': + return function () use ($field, $cb) { + $value = carbon_get_theme_option($field->getBaseName()); + return $cb($value, $field, $this->container); + }; + + default: + return function () use ($cb) { + return $cb(); + }; + } + }; + } + + private function getGraphQLRoot() + { + $context = $this->container->type; + $type_callable = array(Decorator::class, "get_{$context}_container_settings"); + + if (!is_callable($type_callable)) { + return []; + } + + $types = call_user_func($type_callable, $this->container); + + if (!is_array($types)) { + $types = [$types]; + } + + return array_map(function ($type) { + switch ($this->container->type) { + case 'post_meta': + return $this->getGraphQLPostTypeRoot($type); + + case 'term_meta': + return $this->getGraphQLTermTypeRoot($type); + + case 'user_meta': + return 'User'; + + default: + return 'Post'; + } + }, $types); + } + + private function getGraphQLPostTypeRoot(String $type) + { + $post_type_object = \get_post_type_object($type); + + return $post_type_object->graphql_single_name; + } + + private function getGraphQLTermTypeRoot(String $type) + { + return \get_taxonomy($type)->graphql_single_name; + } + + private function getFields() + { + $graphql_fields = array_map(function ($field) { + return Field::create($field); + }, $this->container->get_fields()); + + return array_filter($graphql_fields, function (Field $field) { + return $field->isCompatible(); + }); + } +} diff --git a/src/Field.php b/src/Field.php new file mode 100644 index 0000000..e8739f7 --- /dev/null +++ b/src/Field.php @@ -0,0 +1,192 @@ +field = $field; + } + + static public function create(CrbField $field) + { + return new Self($field); + } + + public function getDescription() + { + return $this->field->get_help_text() ?? ''; + } + + public function isCompatible() + { + return !\in_array($this->getCrbType(), Field::$blacklisted_fields); + } + + public function getType() + { + switch ($this->getCrbType()) { + case 'text': + return $this->getTextType(); + + case 'radio': + case 'select': + return 'Crb_Select'; + + case 'multiselect': + return ['list_of' => 'Crb_Select']; + + case 'media_gallery': + return ['list_of' => 'mediaItem']; + + case 'association': + return ['list_of' => $this->getTypeFromAssociation()]; + + case 'checkbox': + return 'Boolean'; + + default: + return 'String'; + } + } + + public function getBaseName() + { + return $this->field->get_base_name(); + } + + public function getOptions() { + return $this->field->get_options(); + } + + public function getResolver() + { + return [MetaResolver::class, $this->getResolverName()]; + } + + private function getTextType() + { + $attributes = $this->field->get_attributes(); + + $html_type = $attributes['type'] ?? 'text'; + + switch ($html_type) { + case 'number': + return 'Float'; + + default: + return 'String'; + } + } + + private function getCrbType() + { + return $this->field->get_type(); + } + + private function getResolverName() + { + if ($this->getCrbType() === 'association') { + return 'getAssociation'; + } + + $type = $this->getType(); + + if (is_array($type)) { + $type = json_encode($type); + } + + switch ($type) { + case 'String': + case 'Boolean': + return 'getScalar'; + case '{"list_of":"mediaItem"}': + return 'getMediaGallery'; + case 'Crb_Select': + return 'getSelect'; + case '{"list_of":"Crb_Select"}': + return 'getMultiSelect'; + default: + return 'getNull'; + } + } + + private function getTypeFromAssociation() + { + $types = $this->field->get_types(); + + if (count($types) === 1) { + [$type] = $types; + + return $this->getGraphQLTypeFromAssociationType($type); + } + + $type_names = array_map([$this, 'getGraphQLTypeFromAssociationType'], $types); + $union_name = 'Union_' . $this->field->get_base_name(); + + register_graphql_union_type($union_name, [ + 'typeNames' => $type_names, + 'resolveType' => function ($object) { + if ($object instanceof Post) { + $graphql_single_name = \get_post_type_object($object->post_type)->graphql_single_name; + + if ($graphql_single_name === 'post') { + return 'Post'; + } + + return $graphql_single_name; + } + + if ($object instanceof Term) { + return \get_taxonomy($object->taxonomyName)->graphql_single_name; + } + + if ($object instanceof Comment) { + return 'Comment'; + } + + if ($object instanceof User) { + return 'User'; + } + + return ''; + } + ]); + + return $union_name; + } + + private function getGraphQLTypeFromAssociationType($type) + { + switch ($type['type']) { + case 'post': + return \get_post_type_object($type['post_type'])->graphql_single_name; + + case 'term': + return \get_taxonomy($type['taxonomy'])->graphql_single_name; + + case 'user': + return 'user'; + + case 'comment': + return 'comment'; + + default: + return $type['type']; + } + } +} diff --git a/src/MetaResolver.php b/src/MetaResolver.php new file mode 100644 index 0000000..a91e334 --- /dev/null +++ b/src/MetaResolver.php @@ -0,0 +1,81 @@ +getOptions() ?? []; + + return [ + 'id' => $value, + 'value' => $value ?? null, + 'label' => $options[$value] ?? null, + ]; + } + + public static function getMultiSelect($value, Field $field, Container $container, $args, AppContext $context, ResolveInfo $info) + { + $values = $value ?? []; + $options = $field->getOptions() ?? []; + return array_map(function ($value) use ($options) { + return [ + 'id' => $value, + 'value' => $value, + 'label' => $options[$value] ?? $value, + ]; + }, $values); + } + + public static function getAssociation($assocations, Field $field, Container $container, $args, AppContext $context, ResolveInfo $info) + { + return array_map(function ($assocation) use ($context) { + ['type' => $type, 'subtype' => $subtype, 'id' => $id] = $assocation; + + switch ($type) { + case 'post': + $post = DataSource::resolve_post_object($id, $context); + return $post; + + case 'term': + return DataSource::resolve_term_object($id, $context); + + case 'user': + $user = DataSource::resolve_user($id, $context); + return $user; + + case 'comment': + $comment = DataSource::resolve_comment($id, $context); + return $comment; + + default: + return Self::getNull(); + } + }, $assocations); + } + + public static function getNull() + { + return function () { + return null; + }; + } +}