�PNG  IHDR22?�� pHYs  �� OiCCPPhotoshop ICC profilexڝSgTS�=���BK���KoR RB���&*! J�!��Q�EEȠ�����Q, � ��!���������{�kּ������>���� �H3Q5� �B�������.@� $p�d!s�#�~<<+"��x� �M��0���B�\���t�8K�@z�B�@F���&S�`�cb�P-`'������{[�!�� e�Dh;��V�EX0fK�9�-0IWfH���� � 0Q��){`�##x��F�W<�+��*x��<�$9E�[-qWW.(�I+6aa�@.�y�2�4���������x����6��_-��"bb���ϫp@�t~��,/��;�m��%�h^ �u��f�@����W�p�~<�5�j>{�-�]c�K'Xt����o��(�h���w��?�G�%�fI�q^D$.Tʳ?�D��*�A��,���� �`6�B$��BB d�r`)��B(�Ͱ*`/�@4�Qh��p.�U�=p�a��(�� A�a!ڈb�X#����!�H�$ ɈQ"K�5H1R�T UH�=r9�\F��;�2����G1���Q=� �C��7�F� �dt1�����r�=�6��Ыhڏ>C�0��3�l0.��B�8, �c˱"� ���V����cϱw�E� 6wB aAHXLXN�H� $4� 7 �Q�'"��K�&���b21�XH,#��/{�C�7$�C2'��I��T��F�nR#�,��4H#���dk�9�, +ȅ����3��!�[ �b@q��S�(R�jJ��4�e�2AU��Rݨ�T5�ZB���R�Q��4u�9̓IK�����hh�i��t�ݕN��W���G���w ��Ljg(�gw��L�Ӌ�T071���oUX*�*|�� �J�&�*/T����ު U�U�T��^S}�FU3S� Ԗ�U��P�SSg�;���g�oT?�~Y��Y�L�OC�Q��_�� c�x,!k ��u�5�&���|v*�����=���9C3J3W�R�f?�q��tN �(���~���)�)�4L�1e\k����X�H�Q�G�6������E�Y��A�J'\'Gg����S�Sݧ �M=:��.�k���Dw�n��^��Lo��y���}/�T�m���G X� $� �<�5qo</���QC]�@C�a�a�ᄑ��<��F�F�i�\�$�m�mƣ&&!&KM�M�RM��)�;L;L���͢�֙5�=1�2��כ߷`ZxZ,����eI��Z�Yn�Z9Y�XUZ]�F���%ֻ�����N�N���gð�ɶ�����ۮ�m�}agbg�Ů��}�}��= ���Z~s�r:V:ޚΜ�?}����/gX���3��)�i�S��Ggg�s�󈋉K��.�>.���Ƚ�Jt�q]�z���������ۯ�6�i�ܟ�4�)�Y3s���C�Q��? ��0k߬~OCO�g��#/c/�W�װ��w��a�>�>r��>�<7�2�Y_�7��ȷ�O�o�_��C#�d�z����%g��A�[��z|!��?:�e����A���AA�������!h�쐭!��Α�i�P~���a�a��~ '���W�?�p�X�1�5w��Cs�D�D�Dޛg1O9�-J5*>�.j<�7�4�?�.fY��X�XIlK9.*�6nl������� �{�/�]py�����.,:�@L�N8��A*��%�w%� y��g"/�6ш�C\*N�H*Mz�쑼5y$�3�,幄'���L Lݛ:��v m2=:�1����qB�!M��g�g�fvˬe����n��/��k���Y- �B��TZ(�*�geWf�͉�9���+��̳�ې7�����ᒶ��KW-X潬j9�������(�x��oʿ�ܔ���Ĺd�f�f���-�[����n �ڴ �V����E�/��(ۻ��C���<��e����;?T�T�T�T6��ݵa��n��{��4���[���>ɾ�UUM�f�e�I���?�������m]�Nmq����#�׹���=TR��+�G�����w- 6 U����#pDy��� �� :�v�{���vg/jB��F�S��[b[�O�>����z�G��499�?r����C�d�&����ˮ/~�����јѡ�򗓿m|������������x31^�V���w�w��O�| (�h���SЧ��������c3-� cHRMz%������u0�`:�o�_�F5IDATx��ytUս�?�;���fN$$��@B�!�k��� -*�gE���j�O�kQ�ʠ`U�B_ �AÐ0%�Ȕ�@r3ޛ;߳��$�@��v����^�s����������$��ߢ !�W��5�̱'-٘������4�>�Rƫ$�G��0U�H}�_ _� #��k�MJ/=�9Du *ڲ�k�PQ;0��j|�*�$�^컂脛y���>�z×� B��� ��(�:$�ʔ_�6��C!B����MVx��a��ζyY����j��ƋY2�9�F�{r��r�딟F��Q��hY6��� x��[l��RC��������nt@��;P��3��� F�w��7��:�P���ɋ'���mj���Bƪ�$�Q�L��y��߷g��\���z����v5�p�v$�v櫻�`6~K �Y�X����,�:��"�o�e0II"(��,������l_�C���/4�+f�E�!q��:����u����c��樼.UE�@� �]K�W���$-�h]�JG���Dz)�`��.�A/ ���mᏣc�3jK� �!�>���$�Q�!�_�? \�C2(E �M$Z I{�U���s�ǣ���=��@Բe ޻��W�����@ �w����}�9v,)��.}*|"�s��di���,���G�]W�AC�������|<����[��ߕ������K�*�G nߴ!��‹��J�!��6(L�a)��Am��������;adf��:�xm��nh�;֦I��(���o�C~ډa?�=��s����h��E������xp����)���_���t��W�����´;8���`dz=(��@@�+,�;�`�fhl��XȌ�3,uI�~z(J����� �ZX�A����H'�À�㜦�82|��a�n�z2��I���� ���EY�h�ε<�0�q�{��C��v�˅��?"@��χ�x�WS+��N�~�KQ�J�8ل�� _.W��� !TU{�����,��"�� �;�������HI S� ��))�o�1�H�D� ���� (� ;����"Ҕ��,���� �F=���O��&�Ph>�`���y��߲ &S��P�<��'��1s�1cĂ�N����2)����@�u��tv��<�+�-&�����h��[!FSG���' � z z0��<����8d&�����E0�,��<��"!P�b4��� �{S�����X����.�囤���T��hEF��X�q0"-<����h���]�Py*����F⩍��@v�˂�Y08�� .q��a��s����(!<̫1�3�L�H��7sYW�`��qtu�\7��?�@�Y�� %Ka�x�GZ�*W�P��y�����N0r9��Z�c�%���q,��k�����!!,��>���IB��� �x1�Xu�Y���v-���ֺ��N������JP��a�2`�e5��w��u;�6�i���̃��(K���ťa��(l51��JL��O'%���]���ٖ뿉!��{*�p+f ���i8��D�ȑ���ܩ�M��V��ӟz��x�7Z�1i}�^͜���6e����x��)u�d�ˏ�V�eG�\�X1�����=n�U'�~Y����L�\,�(d!x ,�:�TW8� M`r���@��r+v�Q0��>^;˴�VbF��('����� �*O��i��j�Ӆ����^�S3&� ���{������.ی�� N;��NU%d0�Ce\r"�C�b��&t7N�7��� �ȪD�[%l�۪�$��'m?�A9tI�9>��NVL ��� 6v~M�����ъ�B���0����9$&=ώm��W�0���L���&��E���f�f��'+�9��hv��7,���M��6�D�&�^� �����8 D֮�����o��k^z��O��hP�8q�5d*��YVq3�{��������#LX4�?��jP=3�~�؅c�ݰ�̱�ұ��8̲��2�G�����j��έ�9��w7���U�X�w�85C��?!I&���o&����L���I#�^�eV�,�^t5�k� �xs˛\��U�8��."���{���ǵ�l4��ݼ�;��r"���G�/~���gJV�N糦��ZZ^k+NN�D��I$�=OE�z����|*�*{��%k���G��>�`9��b;�s���8,�qs��c��.b��q�%�>r��n�R�Ƨ�%�,�A��le#�8���}; 21�0�b�&�QX�f���q�00����$>�h�N�'�a3��A���/�r ��L5�8�GԲ'�����w;��c#�q1���h$*��V^�k��QĽ�b:i������� ����H F#�_���,��]g����Ը����`RUU���� ��"�qD���>-��ϤI�~��3�̴�����NZ[[555�eeek7�m����YF#[��Đ��R��i��L�H�r ��@��J-�Pŝ��_|�-��6y��ks�V�U~��6���w.����(��0A� 4��f��� /���C�xŊ�ٳ����z=6�����rg���mKo��o�/����a�g���?'9%9��P��斓��~���L{�� �H��{AW��B��$����v�1b�&Mb�ر��ر#�0`�`xMBp! )Z�Nmo�mZ�g�}�eذaWTVV ���'99��C5��&�&+5�bT>|8��PUUETT����&���t��~TE%��'������b�tEGG;RSS�S�N�v��ѯ��t����u1@�ޚ"f��͈���r'L�Ph��m��ƌ�74o��`�x�q�P���NRSS�X,�����l&&&��MFFA!-��`��ҷ�n��Yuu�����x<I�LB%����KԒ50�!�3�bz�����ʸKL���;---�l6[��`��������Ȏ;�����egg4��!D�T��jWujyZ��������K����%����p�^���X,6��sV=�ܖ����6�3�X ���4ҿ��A2�&����_k�;d�� �VfOIEND�B`�
Warning: session_start(): Session cannot be started after headers have already been sent in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 124

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 130

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 131

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 132

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 133

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 134

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 135

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 547

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 548

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 549

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 550

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 551
enum id, so the mapper must resolve ids to human * names before producing WC attributes / variation attributes. * - Prices are integer minor units (e.g. cents). The unit lives alongside. * - Categories are CMS item references that the fetcher pre-resolves onto * the item as `_resolved_categories` (an array of `{name, slug}`). * - Images come from `main-image` + `more-images` on each SKU, plus * `more-images` on the product itself. They must all live in a single * `images[]` array so the importer can build the original_id => attachment * map that variations rely on. * * @internal This class is part of the CLI Migrator feature and should not be used directly. */ class WebflowMapper implements PlatformMapperInterface { /** * Fields to process during mapping (selected via --fields/--exclude-fields). * * @var array */ private array $fields_to_process = array(); /** * Map of ISO currency code => minor-unit count, lazily built from core's locale-info. * * @var array|null */ private ?array $currency_decimals = null; /** * Constructor. * * @param array $args Optional arguments. Recognized keys: * - 'fields': array of field keys to process. Empty/missing means all. */ public function __construct( array $args = array() ) { $this->fields_to_process = $args['fields'] ?? array(); } /** * Maps a raw Webflow product+SKUs item into the importer's standardized array shape. * * @param object $platform_data Webflow product item: `{ product: { id, fieldData: ... }, skus: [...], _resolved_categories?: [...] }`. * @return array Standardized data array. */ public function map_product_data( object $platform_data ): array { $product = $this->extract_product( $platform_data ); $field_data = $this->extract_field_data( $product ); $skus = $this->extract_skus( $platform_data ); $properties = $this->extract_sku_properties( $field_data ); // Needs properties AND more than one SKU. A single-SKU product is imported as simple, // intentionally dropping its lone option (a one-value attribute adds no variation choice). $is_variable = ! empty( $properties ) && count( $skus ) > 1; $wc_data = $this->map_basic_fields( $product, $field_data, $is_variable ); $wc_data['categories'] = $this->should_process( 'categories' ) ? $this->map_categories( $platform_data ) : array(); $wc_data['tags'] = array(); $images = $this->should_process( 'images' ) ? $this->build_images( $field_data, $skus ) : array(); $wc_data['images'] = $images; if ( $is_variable ) { $wc_data = array_merge( $wc_data, $this->map_variable_data( $properties, $skus, $images ) ); } else { $wc_data = array_merge( $wc_data, $this->map_simple_data( $skus ) ); $wc_data['attributes'] = array(); $wc_data['variations'] = array(); } $wc_data['metafields'] = $this->map_seo( $field_data ); return $wc_data; } /** * Webflow list-products items wrap a `product` object and a `skus` array. * * Accept either shape (the wrapped item, or a bare product object) so tests * and downstream callers can be lenient. * * @param object $platform_data Raw item. * @return object Product object (with fieldData). */ private function extract_product( object $platform_data ): object { if ( isset( $platform_data->product ) && is_object( $platform_data->product ) ) { return $platform_data->product; } return $platform_data; } /** * Returns the fieldData object on a product, or an empty stdClass for safety. * * @param object $product Product object. * @return object */ private function extract_field_data( object $product ): object { if ( isset( $product->fieldData ) && is_object( $product->fieldData ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. return $product->fieldData; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. } return new \stdClass(); } /** * Returns the SKUs array off a wrapped item, or empty array. * * @param object $platform_data Raw item. * @return array */ private function extract_skus( object $platform_data ): array { if ( isset( $platform_data->skus ) && is_array( $platform_data->skus ) ) { return $platform_data->skus; } return array(); } /** * Returns the sku-properties array off fieldData, or empty array. * * @param object $field_data Field data. * @return array */ private function extract_sku_properties( object $field_data ): array { $key = 'sku-properties'; if ( isset( $field_data->{$key} ) && is_array( $field_data->{$key} ) ) { return $field_data->{$key}; } return array(); } /** * Map fields common to every product (name, slug, description, status…). * * @param object $product Product object. * @param object $field_data Field data object. * @param bool $is_variable Whether this product has multiple SKUs to expose as variations. * @return array */ private function map_basic_fields( object $product, object $field_data, bool $is_variable ): array { $basic = array(); $basic['is_variable'] = $is_variable; $basic['original_product_id'] = isset( $product->id ) ? (string) $product->id : null; $basic['name'] = isset( $field_data->name ) ? sanitize_text_field( (string) $field_data->name ) : ''; $basic['slug'] = isset( $field_data->slug ) ? sanitize_title( (string) $field_data->slug ) : sanitize_title( $basic['name'] ); $basic['description'] = isset( $field_data->description ) ? wp_kses_post( (string) $field_data->description ) : ''; $short_description_key = 'short-description'; $basic['short_description'] = isset( $field_data->{$short_description_key} ) ? wp_kses_post( (string) $field_data->{$short_description_key} ) : ''; // Status and visibility mirror the source on every import, so a re-run resets any manual // draft/hide a merchant applied to a previously imported product. This is intentional: the // migrator treats Webflow as the source of truth for these fields. $basic['status'] = $this->map_status( $product ); $basic['catalog_visibility'] = 'visible'; if ( isset( $product->createdOn ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. $basic['date_created_gmt'] = (string) $product->createdOn; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. } if ( isset( $product->lastUpdated ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. $basic['date_modified_gmt'] = (string) $product->lastUpdated; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. } $basic['brand'] = null; return $basic; } /** * Map Webflow product publication flags to WC status. * * @param object $product Product object. * @return string */ private function map_status( object $product ): string { $is_archived = ! empty( $product->isArchived ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. $is_draft = ! empty( $product->isDraft ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. if ( $is_archived ) { return 'draft'; } if ( $is_draft ) { return 'draft'; } return 'publish'; } /** * Read the pre-resolved categories the fetcher attached to the item. * * @param object $platform_data Raw item. * @return array */ private function map_categories( object $platform_data ): array { $resolved_key = '_resolved_categories'; if ( ! isset( $platform_data->{$resolved_key} ) || ! is_array( $platform_data->{$resolved_key} ) ) { return array(); } $categories = array(); foreach ( $platform_data->{$resolved_key} as $entry ) { if ( is_array( $entry ) && ! empty( $entry['name'] ) ) { $categories[] = array( 'name' => sanitize_text_field( (string) $entry['name'] ), 'slug' => sanitize_title( (string) ( $entry['slug'] ?? $entry['name'] ) ), ); } elseif ( is_object( $entry ) && ! empty( $entry->name ) ) { $categories[] = array( 'name' => sanitize_text_field( (string) $entry->name ), 'slug' => sanitize_title( (string) ( $entry->slug ?? $entry->name ) ), ); } } return $categories; } /** * Build the unified images array (product gallery + SKU images, deduped by URL). * * The importer requires every image referenced from a variation to also appear * in this top-level array — that's how the original_id => attachment mapping is * populated. We assign stable `original_id`s here (preferring Webflow's fileId, * falling back to a URL hash) so SKU references resolve cleanly later. * * @param object $field_data Product fieldData. * @param array $skus SKUs array. * @return array */ private function build_images( object $field_data, array $skus ): array { $images = array(); $by_url = array(); $featured_url = null; // Product-level gallery: fieldData['more-images']. $more_images_key = 'more-images'; if ( isset( $field_data->{$more_images_key} ) && is_array( $field_data->{$more_images_key} ) ) { foreach ( $field_data->{$more_images_key} as $img ) { $entry = $this->normalize_image_object( $img ); if ( null === $entry ) { continue; } if ( null === $featured_url ) { $featured_url = $entry['src']; } $this->add_unique_image( $images, $by_url, $entry ); } } // SKU main-image + more-images. $main_image_key = 'main-image'; foreach ( $skus as $sku ) { $sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. if ( null === $sku_field ) { continue; } $main_entry = isset( $sku_field->{$main_image_key} ) ? $this->normalize_image_object( $sku_field->{$main_image_key} ) : null; if ( $main_entry ) { if ( null === $featured_url ) { $featured_url = $main_entry['src']; } $this->add_unique_image( $images, $by_url, $main_entry ); } if ( isset( $sku_field->{$more_images_key} ) && is_array( $sku_field->{$more_images_key} ) ) { foreach ( $sku_field->{$more_images_key} as $img ) { $entry = $this->normalize_image_object( $img ); if ( $entry ) { $this->add_unique_image( $images, $by_url, $entry ); } } } } // Mark featured. foreach ( $images as &$image ) { $image['is_featured'] = ( $image['src'] === $featured_url ); } unset( $image ); return $images; } /** * Normalize a Webflow image object into our images-array entry, or return null if unusable. * * @param mixed $img Raw Webflow image entry. * @return array{original_id:string,src:string,alt:?string,is_featured:bool}|null */ private function normalize_image_object( $img ): ?array { if ( ! is_object( $img ) ) { return null; } $url = isset( $img->url ) ? (string) $img->url : ''; if ( '' === $url ) { return null; } $alt = isset( $img->alt ) ? (string) $img->alt : null; $original_id = ''; if ( isset( $img->fileId ) && '' !== (string) $img->fileId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. $original_id = (string) $img->fileId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. } elseif ( isset( $img->id ) && '' !== (string) $img->id ) { $original_id = (string) $img->id; } else { $original_id = 'webflow-' . md5( $url ); } return array( 'original_id' => $original_id, 'src' => $url, 'alt' => $alt, 'is_featured' => false, ); } /** * Append an image entry to $images keyed by URL, deduping repeats. * * @param array $images Image list (mutated). * @param array $by_url URL => index map (mutated). * @param array $entry The image entry to add. * @return void */ private function add_unique_image( array &$images, array &$by_url, array $entry ): void { $url = $entry['src']; if ( isset( $by_url[ $url ] ) ) { return; } $by_url[ $url ] = count( $images ); $images[] = $entry; } /** * Map fields specific to a simple (single-SKU) product. * * @param array $skus SKUs. * @return array */ private function map_simple_data( array $skus ): array { $simple = array( 'sku' => null, 'regular_price' => null, 'sale_price' => null, 'manage_stock' => false, 'stock_quantity' => null, 'stock_status' => 'instock', 'tax_status' => 'taxable', ); if ( empty( $skus ) ) { return $simple; } $sku = $skus[0]; $sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. if ( null === $sku_field ) { return $simple; } if ( $this->should_process( 'price' ) ) { $prices = $this->extract_prices( $sku_field ); $simple['regular_price'] = $prices['regular_price']; $simple['sale_price'] = $prices['sale_price']; } if ( $this->should_process( 'sku' ) && isset( $sku_field->sku ) ) { $simple['sku'] = wc_clean( (string) $sku_field->sku ); } if ( $this->should_process( 'stock' ) ) { $stock = $this->extract_stock( $sku_field ); $simple['manage_stock'] = $stock['manage_stock']; $simple['stock_quantity'] = $stock['stock_quantity']; $simple['stock_status'] = $stock['stock_status']; } if ( $this->should_process( 'weight' ) ) { $simple['weight'] = $this->extract_weight( $sku_field ); } if ( $this->should_process( 'dimensions' ) ) { $simple = array_merge( $simple, $this->extract_dimensions( $sku_field ) ); } return $simple; } /** * Map variable-product data: attribute definitions + per-variation rows. * * @param array $properties sku-properties array. * @param array $skus SKUs array. * @param array $images Already-built images list (to resolve image references). * @return array{attributes: array, variations: array} */ private function map_variable_data( array $properties, array $skus, array $images ): array { $attributes = array(); // Build a lookup: property_id => [ 'name' => string, 'enums' => [ enum_id => option_name ] ]. $property_lookup = array(); $position = 0; foreach ( $properties as $property ) { if ( ! is_object( $property ) || empty( $property->id ) || empty( $property->name ) ) { continue; } $enum_map = array(); $enum_options = array(); $enums = ( isset( $property->enum ) && is_array( $property->enum ) ) ? $property->enum : array(); foreach ( $enums as $enum ) { if ( ! is_object( $enum ) || empty( $enum->id ) || empty( $enum->name ) ) { continue; } $enum_map[ (string) $enum->id ] = (string) $enum->name; $enum_options[] = (string) $enum->name; } $property_lookup[ (string) $property->id ] = array( 'name' => (string) $property->name, 'enums' => $enum_map, ); if ( $this->should_process( 'attributes' ) ) { $attributes[] = array( 'name' => wc_clean( (string) $property->name ), 'options' => array_map( 'wc_clean', $enum_options ), 'position' => $position, 'is_visible' => true, 'is_variation' => true, ); } ++$position; } // Build URL => original_id map so we can resolve a SKU's main-image back to an entry in images[]. $url_to_original_id = array(); foreach ( $images as $image ) { $url_to_original_id[ $image['src'] ] = $image['original_id']; } $variations = array(); $menu_order = 0; foreach ( $skus as $sku ) { $sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase. if ( null === $sku_field ) { continue; } $variation = array( 'original_id' => isset( $sku->id ) ? (string) $sku->id : null, 'sku' => null, 'regular_price' => null, 'sale_price' => null, 'manage_stock' => false, 'stock_quantity' => null, 'stock_status' => 'instock', 'tax_status' => 'taxable', 'attributes' => array(), 'image_original_id' => null, 'menu_order' => $menu_order, ); if ( $this->should_process( 'sku' ) && isset( $sku_field->sku ) ) { $variation['sku'] = wc_clean( (string) $sku_field->sku ); } if ( $this->should_process( 'price' ) ) { $prices = $this->extract_prices( $sku_field ); $variation['regular_price'] = $prices['regular_price']; $variation['sale_price'] = $prices['sale_price']; } if ( $this->should_process( 'stock' ) ) { $stock = $this->extract_stock( $sku_field ); $variation['manage_stock'] = $stock['manage_stock']; $variation['stock_quantity'] = $stock['stock_quantity']; $variation['stock_status'] = $stock['stock_status']; } if ( $this->should_process( 'weight' ) ) { $variation['weight'] = $this->extract_weight( $sku_field ); } if ( $this->should_process( 'dimensions' ) ) { $variation = array_merge( $variation, $this->extract_dimensions( $sku_field ) ); } if ( $this->should_process( 'attributes' ) ) { $variation['attributes'] = $this->resolve_variation_attributes( $sku_field, $property_lookup ); if ( empty( $variation['attributes'] ) ) { wc_get_logger()->debug( sprintf( 'Webflow variation %s resolved to zero attributes; WooCommerce will store it as an "Any" variation, which can collide with sibling variations.', $variation['original_id'] ?? 'unknown' ), array( 'source' => 'wc-migrator' ) ); } } if ( $this->should_process( 'images' ) ) { $main_image_key = 'main-image'; if ( isset( $sku_field->{$main_image_key}->url ) ) { $url = (string) $sku_field->{$main_image_key}->url; if ( isset( $url_to_original_id[ $url ] ) ) { $variation['image_original_id'] = $url_to_original_id[ $url ]; } } } $variations[] = $variation; ++$menu_order; } return array( 'attributes' => $attributes, 'variations' => $variations, ); } /** * Resolve a SKU's `sku-values` (property_id => enum_id) into a human-readable * `attribute_name => option_name` array using the property lookup. * * @param object $sku_field SKU fieldData. * @param array $property_lookup Property lookup map. * @return array */ private function resolve_variation_attributes( object $sku_field, array $property_lookup ): array { $resolved = array(); $values_key = 'sku-values'; if ( ! isset( $sku_field->{$values_key} ) ) { return $resolved; } $values = $sku_field->{$values_key}; if ( ! is_object( $values ) && ! is_array( $values ) ) { return $resolved; } foreach ( (array) $values as $property_id => $enum_id ) { $property_id = (string) $property_id; $enum_id = (string) $enum_id; if ( ! isset( $property_lookup[ $property_id ] ) ) { continue; } $property_name = $property_lookup[ $property_id ]['name']; $option_name = $property_lookup[ $property_id ]['enums'][ $enum_id ] ?? null; if ( null === $option_name ) { continue; } $resolved[ sanitize_text_field( $property_name ) ] = sanitize_text_field( $option_name ); } return $resolved; } /** * Convert Webflow `price.value` (minor units, e.g. cents) into a decimal string, * and detect sale pricing via `compare-at-price`. * * @param object $sku_field SKU fieldData. * @return array{regular_price: ?string, sale_price: ?string} */ private function extract_prices( object $sku_field ): array { $price = $this->price_to_decimal( $sku_field->price ?? null ); $compare_key = 'compare-at-price'; $compare_price = $this->price_to_decimal( $sku_field->{$compare_key} ?? null ); if ( null !== $compare_price && null !== $price && (float) $compare_price > (float) $price ) { return array( 'regular_price' => $compare_price, 'sale_price' => $price, ); } return array( 'regular_price' => $price, 'sale_price' => null, ); } /** * Convert a Webflow money object `{ value: int (minor units), unit: "USD" }` into a decimal string. * * The number of minor units per major unit varies by currency (e.g. JPY has 0, * KWD has 3, most have 2), so the divisor is derived from the currency's * `num_decimals` rather than a hardcoded `/ 100`. * * @param mixed $money Webflow money object. * @return string|null */ private function price_to_decimal( $money ): ?string { if ( ! is_object( $money ) || ! isset( $money->value ) ) { return null; } $minor = (int) $money->value; if ( $minor < 0 ) { return null; } $unit = isset( $money->unit ) ? (string) $money->unit : 'USD'; $decimals = $this->get_currency_decimals( $unit ); return number_format( $minor / ( 10 ** $decimals ), $decimals, '.', '' ); } /** * Get the number of decimal places (minor units) for an ISO currency code. * * Reads core's `i18n/locale-info.php` so the migrator stays in sync with * WooCommerce's own per-currency data. Falls back to 2 for unknown codes. * * @param string $currency ISO 4217 currency code. * @return int */ private function get_currency_decimals( string $currency ): int { if ( null === $this->currency_decimals ) { $this->currency_decimals = array(); $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; if ( is_array( $locale_info ) ) { foreach ( $locale_info as $info ) { if ( isset( $info['currency_code'], $info['num_decimals'] ) ) { $this->currency_decimals[ $info['currency_code'] ] = (int) $info['num_decimals']; } } } } return $this->currency_decimals[ strtoupper( $currency ) ] ?? 2; } /** * Map Webflow inventory shape to WC stock fields. * * @param object $sku_field SKU fieldData. * @return array{manage_stock: bool, stock_quantity: ?int, stock_status: string} */ private function extract_stock( object $sku_field ): array { $inventory = isset( $sku_field->inventory ) && is_object( $sku_field->inventory ) ? $sku_field->inventory : null; if ( null === $inventory ) { return array( 'manage_stock' => false, 'stock_quantity' => null, 'stock_status' => 'instock', ); } $type = isset( $inventory->type ) ? (string) $inventory->type : 'infinite'; if ( 'finite' !== $type ) { return array( 'manage_stock' => false, 'stock_quantity' => null, 'stock_status' => 'instock', ); } $quantity = isset( $inventory->quantity ) ? (int) $inventory->quantity : 0; return array( 'manage_stock' => true, 'stock_quantity' => $quantity, 'stock_status' => $quantity > 0 ? 'instock' : 'outofstock', ); } /** * Extract Webflow SKU dimensions (length, width, height). * * Webflow returns these as raw numerics with no associated unit on the SKU * payload — the unit is a store-level setting on the Webflow side, with no * API representation. We pass through as-is; WooCommerce will interpret them * in whatever `woocommerce_dimension_unit` is configured for the destination * store. Null/zero/non-numeric values are dropped. * * @param object $sku_field SKU fieldData. * @return array{length: ?float, width: ?float, height: ?float} */ private function extract_dimensions( object $sku_field ): array { $dimensions = array( 'length' => null, 'width' => null, 'height' => null, ); foreach ( array_keys( $dimensions ) as $key ) { if ( isset( $sku_field->{$key} ) && is_numeric( $sku_field->{$key} ) && (float) $sku_field->{$key} > 0 ) { $dimensions[ $key ] = (float) $sku_field->{$key}; } } return $dimensions; } /** * Convert Webflow `weight` (with optional `weight-unit`) to the store's weight unit. * * @param object $sku_field SKU fieldData. * @return float|null */ private function extract_weight( object $sku_field ): ?float { if ( ! isset( $sku_field->weight ) ) { return null; } $weight = (float) $sku_field->weight; if ( $weight <= 0 ) { return null; } $unit_key = 'weight-unit'; $source_unit = isset( $sku_field->{$unit_key} ) ? strtolower( (string) $sku_field->{$unit_key} ) : 'lbs'; $source_unit = $this->normalize_weight_unit( $source_unit ); $store_unit = strtolower( (string) get_option( 'woocommerce_weight_unit', 'kg' ) ); if ( $source_unit === $store_unit ) { return $weight; } if ( function_exists( 'wc_get_weight' ) ) { $converted = wc_get_weight( $weight, $store_unit, $source_unit ); return is_numeric( $converted ) ? (float) $converted : $weight; } return $weight; } /** * Normalize a Webflow weight unit string to a wc_get_weight compatible unit. * * @param string $unit Raw unit string. * @return string */ private function normalize_weight_unit( string $unit ): string { $map = array( 'oz' => 'oz', 'lb' => 'lbs', 'lbs' => 'lbs', 'pound' => 'lbs', 'g' => 'g', 'gram' => 'g', 'kg' => 'kg', ); return $map[ $unit ] ?? 'lbs'; } /** * Map Webflow SEO fields (`seo-title`, `seo-description`) into a metafields array. * * @param object $field_data Field data. * @return array */ private function map_seo( object $field_data ): array { $meta = array(); $title_key = 'seo-title'; $desc_key = 'seo-description'; if ( isset( $field_data->{$title_key} ) && '' !== (string) $field_data->{$title_key} ) { $meta['global_title_tag'] = (string) $field_data->{$title_key}; } if ( isset( $field_data->{$desc_key} ) && '' !== (string) $field_data->{$desc_key} ) { $meta['global_description_tag'] = (string) $field_data->{$desc_key}; } return $meta; } /** * Should this field be processed? * * @param string $field_key The field key. * @return bool */ private function should_process( string $field_key ): bool { if ( empty( $this->fields_to_process ) ) { return true; } return in_array( $field_key, $this->fields_to_process, true ); } }