diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php
index cca39895ba..2ba391492b 100644
--- a/src/wp-admin/includes/file.php
+++ b/src/wp-admin/includes/file.php
@@ -498,9 +498,7 @@ function wp_edit_theme_plugin_file( $args ) {
 	if ( false === $written ) {
 		return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) );
 	}
-	if ( 'php' === $extension && function_exists( 'opcache_invalidate' ) ) {
-		opcache_invalidate( $real_file, true );
-	}
+	wp_opcache_invalidate( $real_file, true );
 
 	if ( $is_active && 'php' === $extension ) {
 
@@ -608,9 +606,7 @@ function wp_edit_theme_plugin_file( $args ) {
 
 			// Roll-back file change.
 			file_put_contents( $real_file, $previous_content );
-			if ( function_exists( 'opcache_invalidate' ) ) {
-				opcache_invalidate( $real_file, true );
-			}
+			wp_opcache_invalidate( $real_file, true, false);
 
 			if ( ! isset( $result['message'] ) ) {
 				$message = __( 'Something went wrong.' );
@@ -1741,6 +1737,7 @@ function copy_dir( $from, $to, $skip_list = array() ) {
 					return new WP_Error( 'copy_failed_copy_dir', __( 'Could not copy file.' ), $to . $filename );
 				}
 			}
+			wp_opcache_invalidate( $to . $filename );
 		} elseif ( 'd' === $fileinfo['type'] ) {
 			if ( ! $wp_filesystem->is_dir( $to . $filename ) ) {
 				if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) {
@@ -2276,3 +2273,30 @@ function wp_print_request_filesystem_credentials_modal() {
 	</div>
 	<?php
 }
+
+/**
+ * Attempt to clear opcache if possible and necessary.
+ *
+ * This function can be called safely without having to check the file extension or the availability if the opcache
+ * extension. The results will be cached to improve performance on subsequent calls.
+ *
+ * @since 5.5
+ *
+ * @param string $filepath Path to the file including the extension to which the opcache is to be cleared.
+ * @param bool $force True if the timestamp validation should be skipped.
+ * @param string $match_pattern A full valid regular expression to match against the file name. If matched, the opcache
+ *    will be cleared for that $filepath. The default value checks if $filepath ends with ".php" or ".phtml".
+ *
+ */
+function wp_opcache_invalidate( $filepath, $force = false, $match_pattern = '/\.(?:php|phtml)/i' ) {
+	static $trigger = null;
+	if ( $trigger === null ) {
+		$trigger = function_exists( 'opcache_invalidate' )
+				   && ( ! ini_get( 'opcache.validate_timestamps' ) || ini_get( 'opcache.revalidate_freq' ) === "0" )
+				   && apply_filters( 'wp_opcache_invalidate_allowed', true );
+	}
+
+	if ( $trigger && ( ! $match_pattern || preg_match( $match_pattern, $filepath ) ) ) {
+		opcache_invalidate( $filepath, $force );
+	}
+}
diff --git a/src/wp-admin/includes/update-core.php b/src/wp-admin/includes/update-core.php
index 595ebc053a..0fe76de834 100644
--- a/src/wp-admin/includes/update-core.php
+++ b/src/wp-admin/includes/update-core.php
@@ -1352,6 +1352,7 @@ function _copy_dir( $from, $to, $skip_list = array() ) {
 					return new WP_Error( 'copy_failed__copy_dir', __( 'Could not copy file.' ), $to . $filename );
 				}
 			}
+			wp_opcache_invalidate( $to . $filename );
 		} elseif ( 'd' === $fileinfo['type'] ) {
 			if ( ! $wp_filesystem->is_dir( $to . $filename ) ) {
 				if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) {
