Skip to content

Commit 3b8ef72

Browse files
authored
Merge pull request #317 from plausible/improve_conditional_class_loading
Improved: added a service loader which makes sure requests succeed during updates.
2 parents 7de082a + 6317cf0 commit 3b8ef72

2 files changed

Lines changed: 83 additions & 18 deletions

File tree

src/Plugin.php

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,57 @@ final class Plugin {
1616
* @codeCoverageIgnore
1717
*/
1818
public function load_integrations() {
19-
new Integrations();
19+
$this->load_service( Integrations::class );
20+
}
21+
22+
/**
23+
* Instantiates a service class without taking down the site when its file can't be loaded.
24+
*
25+
* WordPress' updater deletes the plugin directory before copying the new version into place, so a
26+
* request landing in that window finds part of our source tree missing. Because
27+
* @see register_services() runs on plugins_loaded for every request, an unguarded `new` turns that
28+
* into a fatal error on the front end instead of a single disabled feature.
29+
*
30+
* Note that `::class` is resolved at compile time and doesn't trigger the autoloader, so passing a
31+
* missing class here is safe.
32+
*
33+
* @since 2.6.1
34+
*
35+
* @param string $class_name Fully qualified class name.
36+
*
37+
* @return object|null Null when the class couldn't be loaded.
38+
*/
39+
private function load_service( $class_name ) {
40+
if ( ! class_exists( $class_name ) ) {
41+
$this->log_missing_service( $class_name );
42+
43+
return null;
44+
}
45+
46+
return new $class_name();
47+
}
48+
49+
/**
50+
* Logs a service that couldn't be loaded by @see load_service().
51+
*
52+
* Only logged when WP_DEBUG is enabled: during a plugin update this is expected and transient, and
53+
* we don't want to fill production logs with it.
54+
*
55+
* @since 2.6.1
56+
*
57+
* @param string $class_name Fully qualified class name.
58+
*
59+
* @return void
60+
*
61+
* @codeCoverageIgnore
62+
*/
63+
private function log_missing_service( $class_name ) {
64+
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
65+
return;
66+
}
67+
68+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
69+
error_log( sprintf( 'Plausible Analytics: %s could not be loaded and was skipped.', $class_name ) );
2070
}
2171

2272
/**
@@ -44,8 +94,8 @@ public function load_plugin_textdomain() {
4494
* @codeCoverageIgnore
4595
*/
4696
public function load_provisioning() {
47-
new Admin\Provisioning();
48-
new Admin\Provisioning\Integrations();
97+
$this->load_service( Admin\Provisioning::class );
98+
$this->load_service( Admin\Provisioning\Integrations::class );
4999
}
50100

51101
/**
@@ -56,7 +106,7 @@ public function load_provisioning() {
56106
* @codeCoverageIgnore
57107
*/
58108
public function load_settings() {
59-
new Admin\Settings\Page();
109+
$this->load_service( Admin\Settings\Page::class );
60110
}
61111

62112
/**
@@ -82,7 +132,7 @@ public function register() {
82132
* @return void
83133
*/
84134
public function setup() {
85-
new Setup();
135+
$this->load_service( Setup::class );
86136
}
87137

88138
/**
@@ -93,26 +143,25 @@ public function setup() {
93143
* @return void
94144
*/
95145
public function register_services() {
96-
97146
if ( is_admin() ) {
98147
add_action( 'init', [ $this, 'load_settings' ] );
99148
add_action( 'init', [ $this, 'load_provisioning' ] );
100149

101-
new Admin\Upgrades();
102-
new Admin\Filters();
103-
new Admin\Actions();
104-
new Admin\Module();
105-
new Admin\PrivacyPolicy();
150+
$this->load_service( Admin\Upgrades::class );
151+
$this->load_service( Admin\Filters::class );
152+
$this->load_service( Admin\Actions::class );
153+
$this->load_service( Admin\Module::class );
154+
$this->load_service( Admin\PrivacyPolicy::class );
106155
}
107156

108157
add_action( 'init', [ $this, 'load_integrations' ] );
109158

110-
new AdminBar();
111-
new Assets();
112-
new Ajax();
113-
new Compatibility();
114-
new InitOptions();
115-
new Proxy();
116-
new Verification();
159+
$this->load_service( AdminBar::class );
160+
$this->load_service( Assets::class );
161+
$this->load_service( Ajax::class );
162+
$this->load_service( Compatibility::class );
163+
$this->load_service( InitOptions::class );
164+
$this->load_service( Proxy::class );
165+
$this->load_service( Verification::class );
117166
}
118167
}

tests/integration/PluginTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,25 @@
66
namespace Plausible\Analytics\Tests\Integration;
77

88
use Plausible\Analytics\Tests\TestCase;
9+
use Plausible\Analytics\WP\AdminBar;
910
use Plausible\Analytics\WP\Plugin;
1011

1112
class PluginTest extends TestCase {
13+
/**
14+
* A service whose file is missing (e.g. because WordPress is mid-update) should be skipped
15+
* instead of failing the request.
16+
*
17+
* @see Plugin::load_service()
18+
*/
19+
public function testLoadServiceSkipsMissingClass() {
20+
$class = new Plugin();
21+
$method = new \ReflectionMethod( $class, 'load_service' );
22+
$method->setAccessible( true );
23+
24+
$this->assertNull( $method->invoke( $class, '\Plausible\Analytics\WP\ThisClassDoesNotExist' ) );
25+
$this->assertInstanceOf( AdminBar::class, $method->invoke( $class, AdminBar::class ) );
26+
}
27+
1228
/**
1329
* @see Plugin::register()
1430
*/

0 commit comments

Comments
 (0)