From 7d084eab6345e7c9c4a651c988b6856a6e130e50 Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Fri, 27 Mar 2026 17:22:45 +0100 Subject: [PATCH 1/2] API overhaul, phpstan issues --- includes/functions.php | 216 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 includes/functions.php diff --git a/includes/functions.php b/includes/functions.php new file mode 100644 index 00000000..b4c31430 --- /dev/null +++ b/includes/functions.php @@ -0,0 +1,216 @@ +set_tags( $arr ) ) : ''; +} + +/** + * Output the links to the translations in your template + * + * You can call this function directly like that + * + * if ( function_exists ( 'msls_the_switcher' ) ) + * msls_the_switcher(); + * + * or just use it as shortcode [sc_msls] + * + * @package Msls + * @uses get_the_msls + * + * @param string[] $arr + */ +function msls_the_switcher( array $arr = array() ): void { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo msls_get_switcher( $arr ); +} + +/** + * Gets the URL of the country flag-icon for a specific locale + * + * @param string $locale + * + * @return string + */ +function msls_get_flag_url( string $locale ): string { + return ( new \lloc\Msls\MslsOptions() )->get_flag_url( $locale ); +} + +/** + * Gets the description for a blog for a specific locale + * + * @param string $locale + * @param string $preset + * + * @return string + */ +function msls_get_blog_description( string $locale, string $preset = '' ): string { + $blog = msls_blog( $locale ); + + return $blog ? $blog->get_description() : $preset; +} + +/** + * Gets the permalink for a translation of the current post in a given language + * + * @param string $locale + * @param string $preset + * + * @return string + */ +function msls_get_permalink( string $locale, string $preset = '' ): string { + $url = null; + $blog = msls_blog( $locale ); + + if ( $blog ) { + $options = \lloc\Msls\MslsOptions::create(); + $url = $blog->get_url( $options ); + } + + return $url ?? $preset; +} + +/** + * Looks for the MslsBlog instance for a specific locale + * + * @param string $locale + * + * @return \lloc\Msls\MslsBlog|null + */ +function msls_blog( string $locale ): ?\lloc\Msls\MslsBlog { + return msls_blog_collection()->get_blog( $locale ); +} + +/** + * Gets the MslsBlogCollection instance + * + * @return \lloc\Msls\MslsBlogCollection + */ +function msls_blog_collection(): \lloc\Msls\MslsBlogCollection { + return \lloc\Msls\MslsBlogCollection::instance(); +} + +/** + * Gets the MslsOptions instance + * + * @return \lloc\Msls\MslsOptions + */ +function msls_options(): \lloc\Msls\MslsOptions { + return \lloc\Msls\MslsOptions::instance(); +} + +/** + * Gets the MslsContentTypes instance + * + * @return \lloc\Msls\MslsContentTypes + */ +function msls_content_types(): \lloc\Msls\MslsContentTypes { + return \lloc\Msls\MslsContentTypes::create(); +} + +/** + * Gets the MslsPostType instance + * + * @return \lloc\Msls\MslsPostType + */ +function msls_post_type(): \lloc\Msls\MslsPostType { + return \lloc\Msls\MslsPostType::instance(); +} + +/** + * Gets the MslsTaxonomy instance + * + * @return \lloc\Msls\MslsTaxonomy + */ +function msls_taxonomy(): \lloc\Msls\MslsTaxonomy { + return \lloc\Msls\MslsTaxonomy::instance(); +} + +/** + * Gets the MslsOutput instance + * + * @return \lloc\Msls\MslsOutput + */ +function msls_output(): \lloc\Msls\MslsOutput { + return \lloc\Msls\MslsOutput::create(); +} + +/** + * Retrieves the MslsOptionsPost instance. + * + * @param int $id + * @return \lloc\Msls\MslsOptionsPost + */ +function msls_get_post( int $id ): \lloc\Msls\MslsOptionsPost { + return new \lloc\Msls\MslsOptionsPost( $id ); +} + +/** + * Retrieves the MslsOptionsTax instance. + * + * Determines the current query based on conditional tags: + * - is_category + * - is_tag + * - is_tax + * + * @param int $id + * @return \lloc\Msls\OptionsTaxInterface + */ +function msls_get_tax( int $id ): \lloc\Msls\OptionsTaxInterface { + return \lloc\Msls\MslsOptionsTax::create( $id ); +} + +/** + * Retrieves the MslsOptionsQuery instance. + * + * Determines the current query based on conditional tags: + * - is_day + * - is_month + * - is_year + * - is_author + * - is_post_type_archive + * + * @return ?\lloc\Msls\MslsOptionsQuery + */ +function msls_get_query(): ?\lloc\Msls\MslsOptionsQuery { + return \lloc\Msls\MslsOptionsQuery::create(); +} + +/** + * Gets structured language data for all available translations + * + * Returns an array of language entries with locale, alpha2 code, URL, + * label, flag URL, and whether it's the current language. This provides + * programmatic access to the same data used by the switcher output, + * without any HTML rendering. + * + * @param bool $filter When true, only returns languages with existing translations + * + * @return array + */ +function msls_get_languages( bool $filter = false ): array { + return msls_output()->get_languages( $filter ); +} + +/** + * Trivial void function for actions that do not return anything. + * + * @return void + */ +function msls_return_void(): void { +} From 6972d4049624dd75606d9ed52d6bca46c1493f9d Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Fri, 27 Mar 2026 17:25:46 +0100 Subject: [PATCH 2/2] phpstan issues --- .distignore | 1 + .gitignore | 7 +- .phpstan.neon.dist | 4 +- .wp-env.json | 12 +- MultisiteLanguageSwitcher.php | 194 +-------------------- includes/Component/Input/Text.php | 4 +- includes/ContentImport/ContentImporter.php | 28 +-- includes/ContentImport/ImportLogger.php | 6 +- includes/ContentImport/MetaBox.php | 6 +- includes/MslsContentFilter.php | 1 + includes/MslsMain.php | 2 +- includes/MslsOutput.php | 50 +++++- includes/MslsPostTag.php | 7 +- includes/MslsShortCode.php | 7 +- includes/MslsSqlCacher.php | 7 +- includes/deprectated.php | 1 + tests/phpunit/TestMslsOutput.php | 101 +++++++++++ 17 files changed, 199 insertions(+), 239 deletions(-) diff --git a/.distignore b/.distignore index 50a350c3..04d2425f 100644 --- a/.distignore +++ b/.distignore @@ -21,6 +21,7 @@ /src /tests AGENTS.md +CLAUDE.md Changelog.md Diagrams.md README.md diff --git a/.gitignore b/.gitignore index 68b9a68d..e0b109ac 100644 --- a/.gitignore +++ b/.gitignore @@ -4,17 +4,18 @@ .phpunit.cache .phpunit.result.cache .vscode/ -composer.lock -composer.phar +CLAUDE.md assets/js/msls-widget-block/ assets/js/msls.js +composer.lock +composer.phar multisite-language-switcher.zip multisite-language-switcher/ node_modules/ out/ phpunit.xml.bak -tests/coverage/ tests/coverage.xml +tests/coverage/ tests/playwright-report/ tests/playwright-results/ tests/playwright/.env.local diff --git a/.phpstan.neon.dist b/.phpstan.neon.dist index 8e41d58a..3a711d24 100644 --- a/.phpstan.neon.dist +++ b/.phpstan.neon.dist @@ -1,9 +1,7 @@ parameters: - level: 6 + level: 8 paths: - MultisiteLanguageSwitcher.php - includes bootstrapFiles: - tests/phpstan/bootstrap.php - ignoreErrors: - - '/^Class lloc\\Msls\\MslsWidget extends generic class WP_Widget but does not specify its types: T$/' \ No newline at end of file diff --git a/.wp-env.json b/.wp-env.json index 3a407b1f..596deecf 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,9 +1,4 @@ { - "phpVersion": "8.3", - "multisite": true, - "plugins": [ - "." - ], "env": { "tests": { "config": { @@ -19,5 +14,10 @@ }, "mappings": { "wp-content/plugins/multisite-language-switcher": "." - } + }, + "multisite": true, + "phpVersion": "8.3", + "plugins": [ + "." + ] } diff --git a/MultisiteLanguageSwitcher.php b/MultisiteLanguageSwitcher.php index 5e992ea2..86e13a40 100644 --- a/MultisiteLanguageSwitcher.php +++ b/MultisiteLanguageSwitcher.php @@ -49,201 +49,9 @@ define( 'MSLS_PLUGIN_PATH', plugin_basename( __FILE__ ) ); define( 'MSLS_PLUGIN__FILE__', __FILE__ ); + require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/deprectated.php'; - /** - * Get the output for using the links to the translations in your code - * - * @package Msls - * @param mixed $attr - * @return string - */ - function msls_get_switcher( $attr ): string { - $arr = is_array( $attr ) ? $attr : array(); - $obj = apply_filters( 'msls_get_output', null ); - - return ! is_null( $obj ) ? strval( $obj->set_tags( $arr ) ) : ''; - } - - /** - * Output the links to the translations in your template - * - * You can call this function directly like that - * - * if ( function_exists ( 'the_msls' ) ) - * the_msls(); - * - * or just use it as shortcode [sc_msls] - * - * @package Msls - * @uses get_the_msls - * - * @param string[] $arr - */ - function msls_the_switcher( array $arr = array() ): void { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo msls_get_switcher( $arr ); - } - - /** - * Gets the URL of the country flag-icon for a specific locale - * - * @param string $locale - * - * @return string - */ - function msls_get_flag_url( string $locale ): string { - return ( new \lloc\Msls\MslsOptions() )->get_flag_url( $locale ); - } - - /** - * Gets the description for a blog for a specific locale - * - * @param string $locale - * @param string $preset - * - * @return string - */ - function msls_get_blog_description( string $locale, string $preset = '' ): string { - $blog = msls_blog( $locale ); - - return $blog ? $blog->get_description() : $preset; - } - - /** - * Gets the permalink for a translation of the current post in a given language - * - * @param string $locale - * @param string $preset - * - * @return string - */ - function msls_get_permalink( string $locale, string $preset = '' ): string { - $url = null; - $blog = msls_blog( $locale ); - - if ( $blog ) { - $options = \lloc\Msls\MslsOptions::create(); - $url = $blog->get_url( $options ); - } - - return $url ?? $preset; - } - - /** - * Looks for the MslsBlog instance for a specific locale - * - * @param string $locale - * - * @return \lloc\Msls\MslsBlog|null - */ - function msls_blog( string $locale ): ?\lloc\Msls\MslsBlog { - return msls_blog_collection()->get_blog( $locale ); - } - - /** - * Gets the MslsBlogCollection instance - * - * @return \lloc\Msls\MslsBlogCollection - */ - function msls_blog_collection(): \lloc\Msls\MslsBlogCollection { - return \lloc\Msls\MslsBlogCollection::instance(); - } - - /** - * Gets the MslsOptions instance - * - * @return \lloc\Msls\MslsOptions - */ - function msls_options(): \lloc\Msls\MslsOptions { - return \lloc\Msls\MslsOptions::instance(); - } - - /** - * Gets the MslsContentTypes instance - * - * @return \lloc\Msls\MslsContentTypes - */ - function msls_content_types(): \lloc\Msls\MslsContentTypes { - return \lloc\Msls\MslsContentTypes::create(); - } - - /** - * Gets the MslsPostType instance - * - * @return \lloc\Msls\MslsPostType - */ - function msls_post_type(): \lloc\Msls\MslsPostType { - return \lloc\Msls\MslsPostType::instance(); - } - - /** - * Gets the MslsTaxonomy instance - * - * @return \lloc\Msls\MslsTaxonomy - */ - function msls_taxonomy(): \lloc\Msls\MslsTaxonomy { - return \lloc\Msls\MslsTaxonomy::instance(); - } - - /** - * Gets the MslsOutput instance - * - * @return \lloc\Msls\MslsOutput - */ - function msls_output(): \lloc\Msls\MslsOutput { - return \lloc\Msls\MslsOutput::create(); - } - - /** - * Retrieves the MslsOptionsPost instance. - * - * @param int $id - * @return \lloc\Msls\MslsOptionsPost - */ - function msls_get_post( int $id ): \lloc\Msls\MslsOptionsPost { - return new \lloc\Msls\MslsOptionsPost( $id ); - } - - /** - * Retrieves the MslsOptionsTax instance. - * - * Determines the current query based on conditional tags: - * - is_category - * - is_tag - * - is_tax - * - * @param int $id - * @return \lloc\Msls\OptionsTaxInterface - */ - function msls_get_tax( int $id ): \lloc\Msls\OptionsTaxInterface { - return \lloc\Msls\MslsOptionsTax::create( $id ); - } - - /** - * Retrieves the MslsOptionsQuery instance. - * - * Determines the current query based on conditional tags: - * - is_day - * - is_month - * - is_year - * - is_author - * - is_post_type_archive - * - * @return ?\lloc\Msls\MslsOptionsQuery - */ - function msls_get_query(): ?\lloc\Msls\MslsOptionsQuery { - return \lloc\Msls\MslsOptionsQuery::create(); - } - - /** - * Trivial void function for actions that do not return anything. - * - * @return void - */ - function msls_return_void(): void { - } - lloc\Msls\MslsPlugin::init(); lloc\Msls\MslsCli::init(); } diff --git a/includes/Component/Input/Text.php b/includes/Component/Input/Text.php index 149d7279..f8789fbd 100644 --- a/includes/Component/Input/Text.php +++ b/includes/Component/Input/Text.php @@ -14,7 +14,7 @@ final class Text extends Component { protected $key; /** - * @var string + * @var ?string */ protected $value; @@ -48,7 +48,7 @@ public function render(): string { return sprintf( '', esc_attr( $this->key ), - esc_attr( $this->value ), + esc_attr( (string) $this->value ), $this->size, $this->readonly // phpcs:ignore WordPress.Security.EscapeOutput ); diff --git a/includes/ContentImport/ContentImporter.php b/includes/ContentImport/ContentImporter.php index 761ba89f..d13432e0 100644 --- a/includes/ContentImport/ContentImporter.php +++ b/includes/ContentImport/ContentImporter.php @@ -60,7 +60,7 @@ class ContentImporter extends MslsRegistryInstance { * @param ?MslsMain $main */ public function __construct( ?MslsMain $main = null ) { - $this->main = ! is_null( $main ) ? $main : MslsMain::create(); + $this->main = $main ?? MslsMain::create(); } /** @@ -175,7 +175,7 @@ protected function pre_flight_check() { /** * Parses the source blog and post IDs from the $_POST array validating them. * - * @return int[]|bool + * @return array{int, int}|false */ public function parse_sources() { if ( ! MslsRequest::has_var( 'msls_import' ) ) { @@ -202,7 +202,7 @@ protected function get_the_blog_post_ID( $blog_id ) { $id = get_the_ID(); - if ( ! empty( $id ) ) { + if ( false !== $id && $id > 0 ) { restore_current_blog(); return $id; @@ -225,11 +225,11 @@ protected function get_the_blog_post_ID( $blog_id ) { * @param int $blog_id * @param array $data * - * @return bool|int + * @return int */ protected function insert_blog_post( $blog_id, array $data = array() ) { if ( empty( $data ) ) { - return false; + return 0; } switch_to_blog( $blog_id ); @@ -242,7 +242,7 @@ protected function insert_blog_post( $blog_id, array $data = array() ) { } $this->handle( true ); - $this->has_created_post = $post_id > 0 ? $post_id : false; + $this->has_created_post = $post_id > 0 ? $post_id : 0; restore_current_blog(); @@ -312,12 +312,12 @@ public function import_content( ImportCoordinates $import_coordinates, array $po $importers = Map::instance()->make( $import_coordinates ); } - if ( is_null( $this->get_logger() ) ) { - $this->set_logger( new ImportLogger( $import_coordinates ) ); + if ( is_null( $this->logger ) ) { + $this->logger = new ImportLogger( $import_coordinates ); } - if ( is_null( $this->get_relations() ) ) { - $this->set_relations( new Relations( $import_coordinates ) ); + if ( is_null( $this->relations ) ) { + $this->relations = new Relations( $import_coordinates ); } if ( ! empty( $importers ) ) { @@ -341,8 +341,8 @@ public function import_content( ImportCoordinates $import_coordinates, array $po * Fires after the import ran. * * @param ImportCoordinates $import_coordinates - * @param ImportLogger $logger - * @param Relations $relations + * @param ?ImportLogger $logger + * @param ?Relations $relations * * @since TBD */ @@ -353,8 +353,8 @@ public function import_content( ImportCoordinates $import_coordinates, array $po * * @param array $post_fields * @param ImportCoordinates $import_coordinates - * @param ImportLogger $logger - * @param Relations $relations + * @param ?ImportLogger $logger + * @param ?Relations $relations */ return apply_filters( 'msls_content_import_data_after_import', diff --git a/includes/ContentImport/ImportLogger.php b/includes/ContentImport/ImportLogger.php index 23d6749f..ac8d5c53 100644 --- a/includes/ContentImport/ImportLogger.php +++ b/includes/ContentImport/ImportLogger.php @@ -128,7 +128,7 @@ protected function build_nested_array( $path, $what = '' ): array { ); $data = json_decode( $json, true ); - return $data; + return is_array( $data ) ? $data : array(); } /** @@ -137,9 +137,7 @@ protected function build_nested_array( $path, $what = '' ): array { * @return string[] */ protected function build_path( string $where ): array { - $where_path = explode( $this->levels_delimiter, $where ); - - return $where_path; + return explode( $this->levels_delimiter, $where ); } /** diff --git a/includes/ContentImport/MetaBox.php b/includes/ContentImport/MetaBox.php index 9457cef6..606fe6bb 100644 --- a/includes/ContentImport/MetaBox.php +++ b/includes/ContentImport/MetaBox.php @@ -24,7 +24,7 @@ class MetaBox extends MslsRegistryInstance { */ public function render(): void { $post = get_post(); - $mydata = new MslsOptionsPost( $post->ID ); + $mydata = new MslsOptionsPost( $post->ID ?? 0 ); $languages = MslsOptionsPost::instance()->get_available_languages(); $current = MslsBlogCollection::get_blog_language( get_current_blog_id() ); $languages = array_diff_key( $languages, array( $current => $current ) ); @@ -139,7 +139,7 @@ protected function inline_thickbox_html( $output = true, array $data = array() )
$value ) : ?> - + factories() as $slug => $factory ) : ?> details(); ?> @@ -184,7 +184,7 @@ protected function inline_thickbox_html( $output = true, array $data = array() ) ', $post = '

' ) { /* translators: %s: list of languages */ $format = __( 'This post is also available in %s.', 'multisite-language-switcher' ); + $output = ''; if ( has_filter( 'msls_filter_string' ) ) { /** diff --git a/includes/MslsMain.php b/includes/MslsMain.php index c27fa8d4..a876a3bc 100644 --- a/includes/MslsMain.php +++ b/includes/MslsMain.php @@ -38,7 +38,7 @@ final public function __construct( MslsOptions $options, MslsBlogCollection $col $this->collection = $collection; } - public static function create(): object { + public static function create(): static { return new static( msls_options(), msls_blog_collection() ); } diff --git a/includes/MslsOutput.php b/includes/MslsOutput.php index 7d3061e3..3f9c4177 100644 --- a/includes/MslsOutput.php +++ b/includes/MslsOutput.php @@ -24,7 +24,7 @@ class MslsOutput extends MslsMain { const MSLS_GET_TAGS_HOOK = 'msls_output_get_tags'; - public static function init(): object { + public static function init(): static { _deprecated_function( __METHOD__, '2.9.2', 'MslsOutput::create' ); return self::create(); @@ -201,6 +201,54 @@ public function set_tags( array $arr = array() ): MslsOutput { return $this; } + /** + * Gets structured language data for all available translations + * + * @param bool $filter When true, only returns languages with existing translations + * + * @return array + */ + public function get_languages( bool $filter = false ): array { + $blogs = $this->collection->get_filtered( $filter ); + + if ( ! $blogs ) { + return array(); + } + + $mydata = MslsOptions::create(); + $languages = array(); + + foreach ( $blogs as $blog ) { + $language = $blog->get_language(); + $is_current = $this->collection->is_current_blog( $blog ); + + if ( $is_current ) { + $url = $mydata->get_current_link(); + } else { + switch_to_blog( $blog->userblog_id ); + + $url = $mydata->get_permalink( $language ); + + restore_current_blog(); + } + + if ( empty( $url ) ) { + continue; + } + + $languages[] = array( + 'locale' => $language, + 'alpha2' => $blog->get_alpha2(), + 'url' => $url, + 'label' => $blog->get_description(), + 'flag_url' => $this->options->get_flag_url( $language ), + 'current' => $is_current, + ); + } + + return $languages; + } + /** * Returns true if the requirements not fulfilled * diff --git a/includes/MslsPostTag.php b/includes/MslsPostTag.php index 2a825c63..0814a857 100644 --- a/includes/MslsPostTag.php +++ b/includes/MslsPostTag.php @@ -54,8 +54,9 @@ public static function suggest(): void { * * @since 0.9.9 */ - $args = (array) apply_filters( 'msls_post_tag_suggest_args', $args ); - foreach ( get_terms( $args ) as $term ) { + $args = (array) apply_filters( 'msls_post_tag_suggest_args', $args ); + $terms = get_terms( $args ); + foreach ( is_array( $terms ) ? $terms : array() as $term ) { /** * Manipulates the term object before using it * @@ -187,7 +188,7 @@ public function the_input( ?\WP_Term $tag, string $title_format, string $item_fo if ( $mydata->has_value( $language ) ) { $term = get_term( $mydata->$language, $type ); - if ( is_object( $term ) ) { + if ( $term instanceof \WP_Term ) { $icon->set_href( (int) $mydata->$language ); $value = $mydata->$language; $title = $term->name; diff --git a/includes/MslsShortCode.php b/includes/MslsShortCode.php index 4e5cf2a1..9f8ec31f 100644 --- a/includes/MslsShortCode.php +++ b/includes/MslsShortCode.php @@ -12,17 +12,16 @@ public static function init(): void { /** * Renders output using the widget's output * - * @return string|false + * @return string */ - public static function render_widget() { + public static function render_widget(): string { if ( msls_options()->is_excluded() ) { return ''; } ob_start(); the_widget( MslsWidget::class ); - $output = ob_get_clean(); - return $output; + return (string) ob_get_clean(); } } diff --git a/includes/MslsSqlCacher.php b/includes/MslsSqlCacher.php index 0e10bfec..85ad0038 100644 --- a/includes/MslsSqlCacher.php +++ b/includes/MslsSqlCacher.php @@ -93,13 +93,16 @@ public function __get( string $name ) { * @return mixed */ public function __call( string $method, array $args ) { + /** @var callable $callback */ + $callback = array( $this->db, $method ); + if ( 'get_' !== substr( $method, 0, 4 ) ) { - return call_user_func_array( array( $this->db, $method ), $args ); + return call_user_func_array( $callback, $args ); } $result = wp_cache_get( $this->cache_key, self::CACHE_GROUP ); if ( false === $result ) { - $result = call_user_func_array( array( $this->db, $method ), $args ); + $result = call_user_func_array( $callback, $args ); wp_cache_set( $this->cache_key, $result, self::CACHE_GROUP, $this->expire ); } diff --git a/includes/deprectated.php b/includes/deprectated.php index 8af510c6..f2e5b36b 100644 --- a/includes/deprectated.php +++ b/includes/deprectated.php @@ -32,6 +32,7 @@ function get_the_msls( $attr ): string { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound function the_msls( array $arr = array() ): void { _deprecated_function( __FUNCTION__, '2.10.1', 'msls_the_switcher' ); + msls_the_switcher( $arr ); } diff --git a/tests/phpunit/TestMslsOutput.php b/tests/phpunit/TestMslsOutput.php index 280f5f7d..86f6a816 100644 --- a/tests/phpunit/TestMslsOutput.php +++ b/tests/phpunit/TestMslsOutput.php @@ -387,6 +387,107 @@ public function test_get_skips_empty_url(): void { $this->assertEquals( array(), ( new MslsOutput( $options, $collection ) )->get( 0 ) ); } + private function stub_options_create(): void { + Functions\expect( 'is_admin' )->andReturn( false ); + Functions\expect( 'is_front_page' )->andReturn( false ); + Functions\expect( 'is_search' )->andReturn( false ); + Functions\expect( 'is_404' )->andReturn( false ); + Functions\expect( 'is_category' )->andReturn( false ); + Functions\expect( 'is_tag' )->andReturn( false ); + Functions\expect( 'is_tax' )->andReturn( false ); + Functions\expect( 'is_date' )->andReturn( false ); + Functions\expect( 'is_author' )->andReturn( false ); + Functions\expect( 'is_post_type_archive' )->andReturn( false ); + Functions\expect( 'get_queried_object_id' )->andReturn( 42 ); + Functions\expect( 'get_option' )->andReturn( array() ); + } + + public function test_get_languages_empty(): void { + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_filtered' )->once()->with( false )->andReturn( array() ); + + $options = \Mockery::mock( MslsOptions::class ); + + $this->assertEquals( array(), ( new MslsOutput( $options, $collection ) )->get_languages() ); + } + + public function test_get_languages(): void { + $blog_de = \Mockery::mock( MslsBlog::class ); + $blog_de->userblog_id = 2; + $blog_de->shouldReceive( 'get_language' )->andReturn( 'de_DE' ); + $blog_de->shouldReceive( 'get_alpha2' )->andReturn( 'de' ); + $blog_de->shouldReceive( 'get_description' )->andReturn( 'Deutsch' ); + + $blog_en = \Mockery::mock( MslsBlog::class ); + $blog_en->userblog_id = 1; + $blog_en->shouldReceive( 'get_language' )->andReturn( 'en_US' ); + $blog_en->shouldReceive( 'get_alpha2' )->andReturn( 'en' ); + $blog_en->shouldReceive( 'get_description' )->andReturn( 'English' ); + + $options = \Mockery::mock( MslsOptions::class ); + $options->shouldReceive( 'get_flag_url' )->andReturnUsing( + function ( string $language ): string { + return "https://example.com/flags/{$language}.png"; + } + ); + + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_filtered' )->once()->with( false )->andReturn( array( $blog_de, $blog_en ) ); + $collection->shouldReceive( 'is_current_blog' )->with( $blog_de )->andReturn( false ); + $collection->shouldReceive( 'is_current_blog' )->with( $blog_en )->andReturn( true ); + + $this->stub_options_create(); + Functions\expect( 'switch_to_blog' )->once()->with( 2 ); + Functions\expect( 'restore_current_blog' )->once(); + Functions\expect( 'home_url' )->andReturn( 'https://example.com/' ); + Functions\expect( 'get_permalink' )->andReturn( 'https://example.com/post' ); + + $result = ( new MslsOutput( $options, $collection ) )->get_languages(); + + $this->assertCount( 2, $result ); + + $this->assertEquals( 'de_DE', $result[0]['locale'] ); + $this->assertEquals( 'de', $result[0]['alpha2'] ); + $this->assertEquals( 'Deutsch', $result[0]['label'] ); + $this->assertNotEmpty( $result[0]['url'] ); + $this->assertEquals( 'https://example.com/flags/de_DE.png', $result[0]['flag_url'] ); + $this->assertFalse( $result[0]['current'] ); + + $this->assertEquals( 'en_US', $result[1]['locale'] ); + $this->assertEquals( 'en', $result[1]['alpha2'] ); + $this->assertEquals( 'English', $result[1]['label'] ); + $this->assertTrue( $result[1]['current'] ); + } + + public function test_get_languages_skips_empty_url(): void { + $blog = \Mockery::mock( MslsBlog::class ); + $blog->userblog_id = 2; + $blog->shouldReceive( 'get_language' )->andReturn( 'fr_FR' ); + + $options = \Mockery::mock( MslsOptions::class ); + $options->shouldReceive( 'get_flag_url' )->andReturn( '' ); + + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_filtered' )->once()->andReturn( array( $blog ) ); + $collection->shouldReceive( 'is_current_blog' )->with( $blog )->andReturn( false ); + + $this->stub_options_create(); + Functions\expect( 'switch_to_blog' )->once()->with( 2 ); + Functions\expect( 'restore_current_blog' )->once(); + Functions\expect( 'home_url' )->andReturn( '' ); + + $this->assertEmpty( ( new MslsOutput( $options, $collection ) )->get_languages() ); + } + + public function test_get_languages_filter(): void { + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_filtered' )->once()->with( true )->andReturn( array() ); + + $options = \Mockery::mock( MslsOptions::class ); + + $this->assertEquals( array(), ( new MslsOutput( $options, $collection ) )->get_languages( true ) ); + } + public function test_init(): void { Functions\expect( '_deprecated_function' )->once();