diff --git a/classes/class-abilities.php b/classes/class-abilities.php index a95e2b53b..6ac6e7bdb 100644 --- a/classes/class-abilities.php +++ b/classes/class-abilities.php @@ -35,17 +35,15 @@ class Abilities { /** * Holds instance of plugin object. - * - * @var Plugin */ - public $plugin; + public Plugin $plugin; /** * Registered ability objects keyed by namespaced name. * * @var array */ - public $abilities = array(); + public array $abilities = array(); /** * Class constructor. diff --git a/classes/class-ability.php b/classes/class-ability.php index cd13d1c36..bb52162bb 100644 --- a/classes/class-ability.php +++ b/classes/class-ability.php @@ -19,10 +19,8 @@ abstract class Ability { /** * Holds instance of plugin object. - * - * @var Plugin */ - protected $plugin; + protected Plugin $plugin; /** * Class constructor. diff --git a/classes/class-admin.php b/classes/class-admin.php index ea2b0f2b1..824a1ad0a 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -85,101 +85,73 @@ class Admin { /** * Holds Network class - * - * @var Network */ - public $network; + public ?Network $network = null; /** * Holds Live Update class - * - * @var Live_Update */ - public $live_update; + public ?Live_Update $live_update = null; /** * Holds Export class - * - * @var Export */ - public $export; + public ?Export $export = null; /** * Menu page screen id - * - * @var string */ - public $screen_id = array(); + public array $screen_id = array(); /** * List table object - * - * @var List_Table */ - public $list_table = null; + public ?List_Table $list_table = null; /** * Option to disable access to Stream - * - * @var bool */ - public $disable_access = false; + public bool $disable_access = false; /** * Class applied to the body of the admin screen - * - * @var string */ - public $admin_body_class = 'wp_stream_screen'; + public string $admin_body_class = 'wp_stream_screen'; /** * Slug of the records page - * - * @var string */ - public $records_page_slug = 'wp_stream'; + public string $records_page_slug = 'wp_stream'; /** * Slug of the settings page - * - * @var string */ - public $settings_page_slug = 'wp_stream_settings'; + public string $settings_page_slug = 'wp_stream_settings'; /** * Parent page of the records and settings pages - * - * @var string */ - public $admin_parent_page = 'admin.php'; + public string $admin_parent_page = 'admin.php'; /** * Capability name for viewing records - * - * @var string */ - public $view_cap = 'view_stream'; + public string $view_cap = 'view_stream'; /** * Capability name for managing settings - * - * @var string */ - public $settings_cap = WP_STREAM_SETTINGS_CAPABILITY; + public string $settings_cap = WP_STREAM_SETTINGS_CAPABILITY; /** * Total amount of authors to pre-load - * - * @var int */ - public $preload_users_max = 50; + public int $preload_users_max = 50; /** * Admin notices, collected and displayed on proper action - * - * @var array */ - public $notices = array(); + public array $notices = array(); /** * Class constructor. diff --git a/classes/class-alerts.php b/classes/class-alerts.php index ae5ae40d9..5152199f7 100644 --- a/classes/class-alerts.php +++ b/classes/class-alerts.php @@ -48,10 +48,8 @@ class Alerts { /** * Post meta prefix - * - * @var string */ - public $meta_prefix = 'wp_stream'; + public string $meta_prefix = 'wp_stream'; /** * Alert Types diff --git a/classes/class-connectors.php b/classes/class-connectors.php index 8c1fdeaf6..3715180a3 100644 --- a/classes/class-connectors.php +++ b/classes/class-connectors.php @@ -58,37 +58,31 @@ class Connectors { * * @var string[]|null */ - private $available_connectors = null; + private ?array $available_connectors = null; /** * Instantiated connectors, keyed by slug. Filled once by instantiate_connector_classes(). * * @var array|null */ - private $connector_instances = null; + private ?array $connector_instances = null; /** * Registered connectors. - * - * @var array */ - public $connectors = array(); + public array $connectors = array(); /** * Contexts registered to Connectors - * - * @var array */ - public $contexts = array(); + public array $contexts = array(); /** * Action taxonomy terms * * Holds slug to localized label association - * - * @var array */ - public $term_labels = array( + public array $term_labels = array( 'stream_connector' => array(), 'stream_context' => array(), 'stream_action' => array(), @@ -96,10 +90,8 @@ class Connectors { /** * Admin notice messages - * - * @var array */ - protected $admin_notices = array(); + protected array $admin_notices = array(); /** * Class constructor. diff --git a/classes/class-db-driver-wpdb.php b/classes/class-db-driver-wpdb.php index 37b1c01ea..9213283f2 100755 --- a/classes/class-db-driver-wpdb.php +++ b/classes/class-db-driver-wpdb.php @@ -13,24 +13,18 @@ class DB_Driver_WPDB implements DB_Driver { /** * Holds Query class - * - * @var Query */ - protected $query; + protected Query $query; /** * Hold records table name - * - * @var string */ - public $table; + public string $table; /** * Hold meta table name - * - * @var string */ - public $table_meta; + public string $table_meta; /** * Class constructor. @@ -166,7 +160,7 @@ public function get_table_names() { * @param \WP_Stream\Plugin $plugin Instance of the plugin. * @return \WP_Stream\Install */ - public function setup_storage( $plugin ) { + public function setup_storage( $plugin ): Install { return new Install( $plugin ); } diff --git a/classes/class-db-driver.php b/classes/class-db-driver.php index ba16f9992..a19f88cfd 100755 --- a/classes/class-db-driver.php +++ b/classes/class-db-driver.php @@ -51,8 +51,10 @@ public function get_table_names(); * Init storage. * * @param \WP_Stream\Plugin $plugin Instance of the plugin. + * + * @return \WP_Stream\Install */ - public function setup_storage( $plugin ); + public function setup_storage( $plugin ): Install; /** * Purge storage. diff --git a/classes/class-filter-input.php b/classes/class-filter-input.php index d1d88d625..0ef4994a4 100644 --- a/classes/class-filter-input.php +++ b/classes/class-filter-input.php @@ -14,10 +14,8 @@ class Filter_Input { /** * Callbacks to be used for input validation/sanitation. - * - * @var array */ - public static $filter_callbacks = array( + public static array $filter_callbacks = array( FILTER_DEFAULT => null, // Validate. FILTER_VALIDATE_BOOLEAN => 'is_bool', diff --git a/classes/class-form-generator.php b/classes/class-form-generator.php index 8d4f68f8a..a7d9623a3 100644 --- a/classes/class-form-generator.php +++ b/classes/class-form-generator.php @@ -14,10 +14,8 @@ class Form_Generator { /** * List of all registered fields. - * - * @var array */ - public $fields = array(); + public array $fields = array(); /** * Adds a new field to the form. diff --git a/classes/class-install.php b/classes/class-install.php index cdad2b6cb..0a79a63e2 100644 --- a/classes/class-install.php +++ b/classes/class-install.php @@ -20,10 +20,8 @@ class Install { /** * Option key to store database version - * - * @var string */ - public $option_key = 'wp_stream_db'; + public string $option_key = 'wp_stream_db'; /** * Holds version of database at last update @@ -48,10 +46,8 @@ class Install { /** * Holds status of whether it's safe to run Stream or not - * - * @var bool */ - public $update_required = false; + public bool $update_required = false; /** * Holds status of whether the database update worked diff --git a/classes/class-live-update.php b/classes/class-live-update.php index 00a7a0524..0a572e5ce 100644 --- a/classes/class-live-update.php +++ b/classes/class-live-update.php @@ -20,17 +20,13 @@ class Live_Update { /** * User meta key/identifier - * - * @var string */ - public $user_meta_key = 'stream_live_update_records'; + public string $user_meta_key = 'stream_live_update_records'; /** * List table object instance - * - * @var List_Table */ - public $list_table = null; + public ?List_Table $list_table = null; /** * Class constructor. diff --git a/classes/class-network.php b/classes/class-network.php index 15209145c..078c430c5 100644 --- a/classes/class-network.php +++ b/classes/class-network.php @@ -21,17 +21,13 @@ class Network { /** * Network page slug - * - * @var string */ - public $network_settings_page_slug = 'wp_stream_network_settings'; + public string $network_settings_page_slug = 'wp_stream_network_settings'; /** * The option name for the network settings. - * - * @var string */ - public $network_settings_option = 'wp_stream_network'; + public string $network_settings_option = 'wp_stream_network'; /** * Class constructor diff --git a/classes/class-plugin.php b/classes/class-plugin.php index e036de3c2..d9edb7537 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -53,45 +53,33 @@ class Plugin { /** * Holds and manages WordPress Admin configurations. - * - * @var Admin */ - public $admin; + public ?Admin $admin = null; /** * Holds and manages alerts. - * - * @var Alerts */ - public $alerts; + public ?Alerts $alerts = null; /** * Holds and manages alerts lists. - * - * @var Alerts_List */ - public $alerts_list; + public ?Alerts_List $alerts_list = null; /** * Holds and manages WordPress Abilities API integration. - * - * @var Abilities */ - public $abilities; + public ?Abilities $abilities = null; /** * Holds and manages connectors - * - * @var Connectors */ - public $connectors; + public ?Connectors $connectors = null; /** * Holds and manages DB connections. - * - * @var DB */ - public $db; + public ?DB $db = null; /** * Holds and manages records. @@ -102,17 +90,13 @@ class Plugin { /** * Stores and manages WordPress settings. - * - * @var Settings */ - public $settings; + public ?Settings $settings = null; /** * Process DB migrations. - * - * @var Install */ - public $install; + public ?Install $install = null; /** * Backend used to schedule Stream's deferred work (purge / reset). @@ -120,10 +104,8 @@ class Plugin { * Either an {@see AS_Scheduler} (Action Scheduler, default) or a * {@see Cron_Scheduler} (WP-Cron fallback), selected at construction via * the `wp_stream_use_action_scheduler` filter. - * - * @var Scheduler */ - public $scheduler; + public Scheduler $scheduler; /** * Whether the bundled Action Scheduler library was loaded. @@ -131,17 +113,13 @@ class Plugin { * Set from a file_exists() check at construction (see __construct), so it * is reliable on `plugins_loaded` even though AS only declares its as_*() * API later on `init`. - * - * @var bool */ - protected $action_scheduler_available = false; + protected bool $action_scheduler_available = false; /** * URLs and Paths used by the plugin - * - * @var array */ - public $locations = array(); + public array $locations = array(); /** * IP address for the current request to be associated with the log entry. diff --git a/classes/class-query.php b/classes/class-query.php index 1f3d101d4..19d2e3244 100644 --- a/classes/class-query.php +++ b/classes/class-query.php @@ -26,10 +26,8 @@ class Query { /** * Hold the number of records found - * - * @var int */ - public $found_records = 0; + public int $found_records = 0; /** * Database handle. Set from the global $wpdb in the constructor. diff --git a/classes/class-settings.php b/classes/class-settings.php index d2ade1917..f1ce50c06 100644 --- a/classes/class-settings.php +++ b/classes/class-settings.php @@ -32,10 +32,8 @@ class Settings { /** * Network settings key/identifier - * - * @var string */ - public $network_options_key = 'wp_stream_network'; + public string $network_options_key = 'wp_stream_network'; /** * Plugin settings @@ -288,7 +286,12 @@ public function get_option_key() { $option_key = $this->network_options_key; } - return apply_filters( 'wp_stream_settings_option_key', $option_key ); + $filtered_key = apply_filters( 'wp_stream_settings_option_key', $option_key ); + + // Guard against filters returning a non-string: the result is assigned + // to the string-typed Settings::$option_key property, where anything but + // a string would throw a TypeError (XWPENG-47). + return is_string( $filtered_key ) ? $filtered_key : $option_key; } /** @@ -414,9 +417,16 @@ class_exists( '\WP_Ability' ) /** * Filter allows for modification of options fields * - * @return array Array of option fields + * @param array $fields Option fields. + * + * @return array Array of option fields */ - $this->fields = apply_filters( 'wp_stream_settings_option_fields', $fields ); + $filtered_fields = apply_filters( 'wp_stream_settings_option_fields', $fields ); + + // Guard against filters returning a non-array: the value feeds the + // Settings::$fields property, which becomes array-typed in XWPENG-47 — + // a non-array would throw a TypeError once typed. + $this->fields = is_array( $filtered_fields ) ? $filtered_fields : $fields; // Sort option fields in each tab by title ASC. foreach ( $this->fields as $tab => $options ) { @@ -609,21 +619,25 @@ public function get_options() { $option_key = $this->option_key; $defaults = $this->get_defaults( $option_key ); + $options = wp_parse_args( + is_network_admin() ? (array) get_site_option( $option_key, array() ) : (array) get_option( $option_key, array() ), + $defaults + ); + /** * Filter allows for modification of options * - * @param array + * @param array $options Options. + * @param string $option_key Option key. * * @return array Updated array of options */ - return apply_filters( - 'wp_stream_settings_options', - wp_parse_args( - is_network_admin() ? (array) get_site_option( $option_key, array() ) : (array) get_option( $option_key, array() ), - $defaults - ), - $option_key - ); + $filtered = apply_filters( 'wp_stream_settings_options', $options, $option_key ); + + // Guard against filters returning a non-array: the result is assigned + // to the array-typed Settings::$options property, where anything but an + // array would throw a TypeError (XWPENG-47). + return is_array( $filtered ) ? $filtered : $options; } /** diff --git a/rector.php b/rector.php index d8da00d2b..9eff99ad7 100644 --- a/rector.php +++ b/rector.php @@ -12,6 +12,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; use Rector\ValueObject\PhpVersion; return RectorConfig::configure() @@ -35,9 +36,25 @@ __DIR__ . '/node_modules', __DIR__ . '/tests', __DIR__ . '/vendor', - // Per-rule skips are appended by the PR that registers the rule (§3, §4). + TypedPropertyFromAssignsRector::class => array( + // Public extension-point bases. Children are typed with the + // parent in later PRs (Q7). Directory skips keep this PR inside + // classes/ so a first-pass run cannot type a child while its + // parent stays untyped (T1.3). + __DIR__ . '/classes/class-connector.php', + __DIR__ . '/classes/class-alert-type.php', + __DIR__ . '/classes/class-alert-trigger.php', + __DIR__ . '/classes/class-exporter.php', + __DIR__ . '/connectors', + __DIR__ . '/alerts', + __DIR__ . '/exporters', + ), ) ) ->withPhpVersion( PhpVersion::PHP_82 ) ->withCache( __DIR__ . '/artifacts/rector' ) - ->withRules( array() ); + ->withRules( array() ) + ->withConfiguredRule( + TypedPropertyFromAssignsRector::class, + array( TypedPropertyFromAssignsRector::INLINE_PUBLIC => true ) + ); diff --git a/tests/phpunit/unit/Connectors_Unit_Test.php b/tests/phpunit/unit/Connectors_Unit_Test.php index 44efca14f..70d89d46b 100644 --- a/tests/phpunit/unit/Connectors_Unit_Test.php +++ b/tests/phpunit/unit/Connectors_Unit_Test.php @@ -331,11 +331,11 @@ private function mock_connector( $name = 'unit-stub' ) { * @return Connectors */ private function make_connectors() { - $plugin = Mockery::mock(); + $plugin = Mockery::mock( Plugin::class ); $plugin->locations = array( 'dir' => '', ); - $plugin->admin = Mockery::mock(); + $plugin->admin = Mockery::mock( Admin::class ); $connectors = ( new ReflectionClass( Connectors::class ) )->newInstanceWithoutConstructor(); $connectors->plugin = $plugin; diff --git a/tests/phpunit/unit/bootstrap.php b/tests/phpunit/unit/bootstrap.php index 1a2a69b80..7bda41055 100644 --- a/tests/phpunit/unit/bootstrap.php +++ b/tests/phpunit/unit/bootstrap.php @@ -33,3 +33,7 @@ function wp_stream_unit_autoload( $class_name ) { } spl_autoload_register( 'wp_stream_unit_autoload' ); + +if ( ! defined( 'WP_STREAM_SETTINGS_CAPABILITY' ) ) { + define( 'WP_STREAM_SETTINGS_CAPABILITY', 'manage_options' ); +}