From bf1c630ede3d93da8b07e1cd8240e2dff53a152d Mon Sep 17 00:00:00 2001 From: Tung Du Date: Thu, 31 Aug 2023 14:07:11 +0700 Subject: [PATCH] Wip: rebased with trunk --- composer.json | 7 ++ composer.lock | 18 ++++ legacy/config.example.php | 3 + migrator.php => legacy/migrator.php | 0 plugin.php | 13 +++ src/CLI.php | 63 +++++++++++ src/Interfaces/Source.php | 13 +++ src/Main.php | 61 +++++++++++ src/Migrators/Product.php | 103 ++++++++++++++++++ src/Registry/AbstractDependencyType.php | 56 ++++++++++ src/Registry/Container.php | 100 ++++++++++++++++++ src/Registry/FactoryType.php | 22 ++++ src/Registry/SharedType.php | 36 +++++++ src/Sources.php | 135 ++++++++++++++++++++++++ src/Sources/Shopify.php | 60 +++++++++++ src/Workers.php | 44 ++++++++ 16 files changed, 734 insertions(+) create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 legacy/config.example.php rename migrator.php => legacy/migrator.php (100%) create mode 100644 plugin.php create mode 100644 src/CLI.php create mode 100644 src/Interfaces/Source.php create mode 100644 src/Main.php create mode 100644 src/Migrators/Product.php create mode 100644 src/Registry/AbstractDependencyType.php create mode 100644 src/Registry/Container.php create mode 100644 src/Registry/FactoryType.php create mode 100644 src/Registry/SharedType.php create mode 100644 src/Sources.php create mode 100644 src/Sources/Shopify.php create mode 100644 src/Workers.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ca70529 --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "Migrator\\": "src/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..fd0bcbc --- /dev/null +++ b/composer.lock @@ -0,0 +1,18 @@ +{ + "_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": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/legacy/config.example.php b/legacy/config.example.php new file mode 100644 index 0000000..204d520 --- /dev/null +++ b/legacy/config.example.php @@ -0,0 +1,3 @@ +init(); +} ); diff --git a/src/CLI.php b/src/CLI.php new file mode 100644 index 0000000..10e91dc --- /dev/null +++ b/src/CLI.php @@ -0,0 +1,63 @@ +get( + Migrators\Product::class, + [ 'source' => $assoc_args['source'] ] + ); + $migrator->run( $assoc_args ); + } +} \ No newline at end of file diff --git a/src/Interfaces/Source.php b/src/Interfaces/Source.php new file mode 100644 index 0000000..9b48d92 --- /dev/null +++ b/src/Interfaces/Source.php @@ -0,0 +1,13 @@ +register_dependencies(); + + add_action( 'cli_init', function() { + \WP_CLI::add_command( 'migrator', 'Migrator\\CLI' ); + } ); + } + + private function register_dependencies() { + $container = self::container( true ); + + // Passing a container instance to the Sources class to manage sources. + $container->register( Sources::class, function() { + return new Sources( new Container() ); + } ); + + $container->register( Workers::class, function() { + return new Workers(); + } ); + + $container->register( + Migrators\Product::class, + $container->factory( function( Container $container, $parameters ) { + return new Product( + $container->get( Sources::class )->get_source( $parameters ), + $container->get( Workers::class )->get_workers( 'product' ) + ); + } ) + ); + } + + /** + * Loads the dependency injection container. + * + * @param boolean $reset Used to reset the container to a fresh instance. + * Note: this means all dependencies will be + * reconstructed. + */ + public static function container( $reset = false ) { + static $container; + if ( + ! $container instanceof Container + || $reset + ) { + $container = new Container(); + } + return $container; + } +} \ No newline at end of file diff --git a/src/Migrators/Product.php b/src/Migrators/Product.php new file mode 100644 index 0000000..289b835 --- /dev/null +++ b/src/Migrators/Product.php @@ -0,0 +1,103 @@ +source = $source; + $this->workers = $workers; + } + + public function run( $args = [] ) { + $this->args = $args; + + array_map( + function( $original_product ) { + $this->migrate( $original_product ); + }, + $this->source->get_products() + ); + } + + private function migrate( $original_product ) { + $product = $this->get_existing( $original_product ); + + if ( ! $product && $this->is_sync() ) { + return; + } + + if ( ! $product ) { + $product = new \WC_Product(); + $product->update_meta_data( '_original_product_id', $original_product['id'] ); + $product->save(); + } + + // Processing the product data. + foreach( $this->workers as $identifier => $worker ) { + try { + call_user_func( $worker, $product, $original_product ); + } catch ( \Exception $e ) { + // Log the error. + } + } + + $product->save(); + } + + private function is_sync() { + return isset( $this->args['sync'] ) && $this->args['sync']; + } + + private function get_existing( $original_product ) { + $id = $original_product['id'] ?? 0; + $sku = $original_product['sku'] ?? ''; + $slug = $original_product['slug'] ?? '' ; + + // Try finding the product by original product ID. + if ( $id ) { + $woo_products = wc_get_products( + array( + 'limit' => 1, + 'meta_key' => '_original_product_id', + 'meta_value' => $id, + ) + ); + + if ( count( $woo_products ) === 1 && is_a( $woo_products[0], 'WC_Product' ) ) { + return wc_get_product( $woo_products[0] ); + } + } + + // Check if the product already exists in Woo by SKU. Only if the + // product is a single product. + if ( $sku ) { + $woo_product = wc_get_product_id_by_sku( $sku ); + + if ( $woo_product ) { + return wc_get_product( $woo_product ); + } + } + + // Check if the product already exists in Woo by slug. + if ( $slug ) { + $woo_product = get_page_by_path( $slug, OBJECT, 'product' ); + + if ( $woo_product ) { + return wc_get_product( $woo_product ); + } + } + + return null; + } +} diff --git a/src/Registry/AbstractDependencyType.php b/src/Registry/AbstractDependencyType.php new file mode 100644 index 0000000..bb60c95 --- /dev/null +++ b/src/Registry/AbstractDependencyType.php @@ -0,0 +1,56 @@ +callable_or_value = $callable_or_value; + } + + /** + * Resolver for the internal dependency value. + * + * @param Container $container The Dependency Injection Container. + * @param array $parameters The parameters to pass to the callback. + * + * @return mixed + */ + protected function resolve_value( Container $container, $parameters ) { + $callback = $this->callable_or_value; + return \is_callable( $callback ) + ? $callback( $container, $parameters ) + : $callback; + } + + /** + * Retrieves the value stored internally for this DependencyType + * + * @param Container $container The Dependency Injection Container. + * @param array $parameters The parameters to pass to the callback. + * + * @return void + */ + abstract public function get( Container $container, $parameters ); +} diff --git a/src/Registry/Container.php b/src/Registry/Container.php new file mode 100644 index 0000000..4277198 --- /dev/null +++ b/src/Registry/Container.php @@ -0,0 +1,100 @@ +register( MyClass::class, $container->factory( $mycallback ) ); + * ``` + * + * @param Closure $instantiation_callback This will be invoked when the + * dependency is required. It will + * receive an instance of this + * container so the callback can + * retrieve dependencies from the + * container. + * + * @return FactoryType An instance of the FactoryType dependency. + */ + public function factory( Closure $instantiation_callback ) { + return new FactoryType( $instantiation_callback ); + } + + /** + * Interface for registering a new dependency with the container. + * + * By default, the $value will be added as a shared dependency. This means + * that it will be a single instance shared among any other classes having + * that dependency. + * + * If you want a new instance every time it's required, then wrap the value + * in a call to the factory method (@see Container::factory for example) + * + * Note: Currently if the provided id already is registered in the container, + * the provided value is ignored. + * + * @param string $id A unique string identifier for the provided value. + * Typically it's the fully qualified name for the + * dependency. + * @param mixed $value The value for the dependency. Typically, this is a + * closure that will create the class instance needed. + */ + public function register( $id, $value ) { + if ( empty( $this->registry[ $id ] ) ) { + if ( ! $value instanceof FactoryType ) { + $value = new SharedType( $value ); + } + $this->registry[ $id ] = $value; + } + } + + /** + * Interface for retrieving the dependency stored in the container for the + * given identifier. + * + * @param string $id The identifier for the dependency being + * retrieved. + * @param array $parameters The parameters to pass to the callback. + * @throws Exception If there is no dependency for the given identifier in + * the container. + * + * @return mixed Typically a class instance. + */ + public function get( $id, $parameters = [] ) { + if ( ! isset( $this->registry[ $id ] ) ) { + // this is a developer facing exception, hence it is not localized. + throw new Exception( + sprintf( + 'Cannot construct an instance of %s because it has not been registered.', + $id + ) + ); + } + return $this->registry[ $id ]->get( $this, $parameters ); + } +} diff --git a/src/Registry/FactoryType.php b/src/Registry/FactoryType.php new file mode 100644 index 0000000..3b75486 --- /dev/null +++ b/src/Registry/FactoryType.php @@ -0,0 +1,22 @@ +resolve_value( $container, $parameters ); + } +} diff --git a/src/Registry/SharedType.php b/src/Registry/SharedType.php new file mode 100644 index 0000000..94ccf5d --- /dev/null +++ b/src/Registry/SharedType.php @@ -0,0 +1,36 @@ +shared_instance ) ) { + $this->shared_instance = $this->resolve_value( + $container, + $parameters + ); + } + return $this->shared_instance; + } +} diff --git a/src/Sources.php b/src/Sources.php new file mode 100644 index 0000000..0fedf00 --- /dev/null +++ b/src/Sources.php @@ -0,0 +1,135 @@ +container = $container; + $this->register( Shopify::class ); + } + + public function register( $class ) { + if ( ! is_subclass_of( $class, Source::class ) ) { + throw new \Exception( 'Class must implement the Source interface.' ); + } + + $this->sources[ $class::IDENTIFIER ] = $class; + $this->credentials[ $class::IDENTIFIER ] = $class::CREDENTIALS; + $this->container->register( $class, $this->container->factory( function( $container, $parameters ) use ( $class ) { + return new $class( $parameters ); + } ) ); + } + + public function get_source( $args ) { + $source = false; + + if ( ! empty( $args['source'] ) ) { + try { + $source = $this->container->get( $this->sources[ $args['source'] ], $args ); + } catch ( \Exception $e ) { + $source = false; + } + } + + if ( $source ) { + return $source; + } + + if ( empty( $args['source'] ) ) { + $source_config = $this->get_default_source_config(); + } else { + $source_config = $this->detect_source_config( $args['source'], $args ); + } + + $source = $this->container->get( $this->sources[ $source_config['identifier'] ], $source_config['credentials'] ); + + if ( ! $source ) { + throw new \Exception( 'Source not found.' ); + } + + return $source; + } + + private function detect_source_config( $source, $args ) { + $enable_sources_config = $this->get_enabled_sources_config(); + + $source_config = array_filter( $enable_sources_config, function( $source_config ) use ( $source ) { + return $source_config['identifier'] === $source; + } ); + + if ( empty( $source_config ) ) { + return false; + } + + $searches = array_intersect_key( $args, $this->credentials[ $source ] ); + + $source_config = array_filter( $source_config, function( $source_config ) use ( $searches ) { + return empty( array_diff_assoc( $searches, $source_config['credentials'] ) ); + } ); + + if ( empty( $source_config ) ) { + return false; + } + + return current( $source_config ); + } + + private function get_default_source_config() { + return current( $this->get_enabled_sources_config() ); + } + + /** + * Each source configuration is an array with the following keys: + * - identifier: The source type identifier. + * - credentials: The source configuration. + * - enabled: Whether this source is enabled. + * + * @return array Array of source configurations. + */ + private function get_available_sources_config() { + $config = []; + /** + * If the config file exists, use it. + */ + if ( file_exists( __DIR__ . '/../config.php' ) ) { + $config = include __DIR__ . '/../config.php'; + } + + /** + * Lastly, try to get the sources from the options table. This also + * allows us to filter the sources using the `pre_option_migrator_sources` filter. + */ + if ( empty( $config ) ) { + $config = get_option( 'migrator_sources', [] ); + } + + if ( empty( $config ) ) { + throw new \Exception( 'No sources available.' ); + } + + return $config; + } + + private function get_enabled_sources_config() { + $available_sources_config = $this->get_available_sources_config(); + + $enable_sources = array_filter( $available_sources_config, function( $source ) { + return ! empty( $source['enabled'] ); + } ); + + if ( empty( $enable_sources ) ) { + throw new \Exception( 'No sources enabled.' ); + } + + return $enable_sources; + } +} \ No newline at end of file diff --git a/src/Sources/Shopify.php b/src/Sources/Shopify.php new file mode 100644 index 0000000..216e54f --- /dev/null +++ b/src/Sources/Shopify.php @@ -0,0 +1,60 @@ +validate_credentials( $args ); + + $this->access_token = $args['access_token']; + $this->domain = $args['domain']; + } + + public function validate_credentials( $args ) { + if ( empty( $args['access_token'] ) ) { + throw new \Exception( 'Access token not set.' ); + } + + if ( empty( $args['domain'] ) ) { + throw new \Exception( 'Store domain not set.' ); + } + } + + public function get_products() { + return [ + [ + 'id' => 1, + 'title' => 'Product 1', + 'price' => 10, + 'slug' => 'product-1', + ], + [ + 'id' => 2, + 'title' => 'Product 2', + 'price' => 20, + 'slug' => 'product-2', + ], + [ + 'id' => 3, + 'title' => 'Product 3', + 'price' => 30, + 'slug' => 'product-3', + ], + ]; + } + + public function get_orders() { + return []; + } +} diff --git a/src/Workers.php b/src/Workers.php new file mode 100644 index 0000000..2728915 --- /dev/null +++ b/src/Workers.php @@ -0,0 +1,44 @@ + '', + 'migrator' => '', + 'message' => '', + 'data_callback' => null, + ] + ); + + if ( ! $args['identifier'] && ! is_string( $args['identifier'] ) ) { + throw new \Exception( '$identifier must be presented' ); + } + + if ( ! $args['migrator'] && ! is_string( $args['migrator'] ) ) { + throw new \Exception( '$migrator must be presented' ); + } + + if ( ! is_null( $args['data_callback'] ) && ! is_callable( $args['data_callback'] ) ) { + throw new \Exception( '$data_callback must be a callable function.' ); + } + + $this->workers[ $args['migrator'] ][ $args['identifier'] ] = $args['data_callback']; + } + + public function get_workers( $migrator ) { + $workers = $this->workers[ $migrator ] ?? false; + + if ( ! $workers ) { + throw new \Exception( 'No workers found for migrator: ' . $migrator ); + } + + return $workers; + } +} \ No newline at end of file