[wp-trac] [WordPress Trac] #20103: Rewrite get_themes() and other enemies of sanity

WordPress Trac wp-trac at lists.automattic.com
Mon Feb 27 22:09:44 UTC 2012


#20103: Rewrite get_themes() and other enemies of sanity
--------------------------+------------------
 Reporter:  nacin         |       Owner:
     Type:  defect (bug)  |      Status:  new
 Priority:  normal        |   Milestone:  3.4
Component:  Themes        |     Version:  1.5
 Severity:  normal        |  Resolution:
 Keywords:                |
--------------------------+------------------
Changes (by nacin):

 * milestone:  Awaiting Review => 3.4


Comment:

 [attachment:wp_theme.diff]

 = Introducing the WP_Theme class =

 As the ticket promises, WP_Theme replaces get_themes() and other enemies
 of sanity. Each theme is its own object, decorated on construction and
 packed with 21 main public methods, four public static methods, and a few
 internal helpers. Full support for multiple theme roots and directories of
 themes within a theme root. The handling for broken-ness and odd
 situations is smoother than get_themes().

 {{{
 #!php
 array wp_get_themes( $args = array() );
 }}}

 To fetch a array of WP_Theme objects, call wp_get_themes(). wp_get_themes(
 $args ) takes three arguments that filter the results: errors, allowed,
 and blog_id.

  * '''errors''' (Default: FALSE) - Filters themes based on whether they
 have errors. True returns only themes with errors, False only themes
 without errors, Null if all themes should be returned.
  * '''allowed''' (Default: NULL) - Filters themes based on whether they
 are allowed on the site or network. (Multisite only.) Valid options are
 'network' (allowed on the network), 'site' (allowed on the site specified
 by the blog_id argument), true (allowed at either level), false (not
 allowed).
  * '''blog_id''' (Default: current) - Controls which site should be
 checked for the allowed filter. Only works, of course, in multisite.

 {{{
 #!php
 WP_Theme wp_get_theme( $stylesheet = null, $theme_root = null );
 }}}

 To fetch a single theme, call wp_get_theme(). This will return a single
 WP_Theme object for the requested theme.

 It takes two argumements: $stylesheet and $theme_root. If $stylesheet is
 omitted, the current stylesheet (via get_stylesheet()) will be used. If
 the $theme_root is omitted, get_raw_theme_root($stylesheet) will be used.
 Thus, to get the current theme, you may call wp_get_theme() with no
 arguments.

 wp_get_theme() replaces get_theme(). get_theme() previously used
 get_current_theme() to get the name-keyed theme array from get_themes().
 Not only are we no longer juggling arrays keyed by the theme name, we're
 not even processing every theme into memory to get a single WP_Theme
 object. Nice.

 == Handling Errors ==

 Currently, search_theme_directories() and get_themes() will build a
 separate array of broken themes, $wp_broken_themes. This array is then
 returned by get_broken_themes().

 With WP_Theme, errors like no template, invalid parent themes, or
 unreadable stylesheets can be nicely caught. Errors are populated into an
 errors property, accessible via WP_Theme->errors(). It is either null or
 no errors, or a WP_Error object. wp_get_themes( array( 'errors' => true )
 ) enables one to loop through and pull out error messages and codes from a
 WP_Error object. In some cases, the current theme could 'error' but it
 still works, and we have access to what went wrong.

 == Network Support ==

 WP_Theme encapsulates the logic for allowed themes on a network, with
 static methods to fetch all allowed themes on a network, themes allowed at
 the site level, and a merge of both lists (all themes allowed for a single
 site).

 Each theme object then has a simple is_allowed( $check = 'both', $blog_id
 = 0 ) method to allow for checking whether it is allowed.

 The okay-named function get_site_allowed_themes() and the terribly named
 function wpmu_get_blog_allowedthemes() are both deprecated in favor of
 static class methods.

 == Outputting Headers ==

 What started me on WP_Theme was that get_themes() could not be
 internationalized in any sane way (#15858). The data usually came out of
 get_theme_data() already cleaned and even marked up (such as with
 wptexturize), making translation difficult or impossible. Since the data
 was keyed by name, changing that value would break quite a bit. As
 get_themes() already stored a lot of data in memory (and blows up
 memcached on WP.com, as they try to store it persistently), adding more
 keys didn't make sense.

 {{{
 #!php
 mixed WP_Theme->get( $header )
 mixed WP_Theme->display( $header, $markup = true, $translate = true )
 }}}

 WP_Theme has two main method for gathering header data: get() and
 display(). Once you call get(), the data has already been sanitized,
 either by strip_tags(), kses, or esc_url, depending on the header. (Tags
 are returned as an array.) By calling display(), the data is translated,
 and then marked up. (Even Tags — the array is translated for known tags.)
 The textdomain is automatically loaded if necessary.

 display() returns data. It does not echo it. It is formatting the data for
 display, and would roughly be analogous to get_bloginfo( 'name', 'display'
 ).

 get('Author') returns Andrew Nacin. display('Author') marks that up with
 <a> tags and a link to http://nacin.com (being AuthorURI).

 display() also allows you to decline markup and translation with
 additional arguments, but these are mainly used for backwards
 compatibility or to allow fields to be searched through without Author
 HTML and the like.

 Private methods sanitize_header(), translate_header(), and markup_header()
 support get() and display() in their endeavors.

 == Helpers ==

 WP_Theme has a number of public methods that corresponds to the global
 function acting on the active theme. These include:
 {{{
 #!php
 bool   WP_Theme->is_child_theme()
 string WP_Theme->get_stylesheet()
 string WP_Theme->get_template()
 string WP_Theme->get_stylesheet_directory()
 string WP_Theme->get_template_directory()
 string WP_Theme->get_stylesheet_directory_uri()
 string WP_Theme->get_template_directory_uri()
 string WP_Theme->get_theme_root()
 string WP_Theme->get_theme_root_uri()
 bool   WP_Theme->load_textdomain()
 array  WP_Theme->get_page_templates()
 }}}

 In some cases (notably, get_page_templates()), the global function now
 wraps the method.

 == Screenshots ==

 {{{
 #!php
 string function get_screenshot( $uri = 'relative' )
 int    function get_screenshot_count()
 array  function get_screenshots()
 }}}

 3.4 means multiple screenshots. WP_Theme has you covered. Three public
 methods provide the screenshot URL (absolute URL, or relative to the
 stylesheet directory), the total count of screenshots, and an array of
 each screenshot filename.

 == Template and Stylesheet Files ==

 {{{
 #!php
 array WP_Theme->get_files( $type = null )
 }}}

 get_themes() stored the template and stylesheet files in the array, which
 resulted in longer processing times and, especially since absolute paths
 were added in 2.9, an explosion in memory usage.

 WP_Theme handles traversing the PHP and CSS files theme directories ''on
 demand''. As these are only used in the theme editor (and only for the
 current theme), this is a pretty huge performance and memory boost.

 == Full Backwards Compatibility ==

 WP_Theme is 100% backwards compatible. Because it implements ArrayAccess,
 you can take a theme object and look for keys like 'Template Files',
 'Stylesheet', and 'Version' — just as they were returned by get_themes().
 WP_Theme also includes a magic getter, handling all properties that were
 returned by current_theme_info() (which returned stdClass in 3.3). This
 enables us to pass a fully decorated WP_Theme object as an existing action
 argument, without plugins choking because they expected an array or an
 older object.

 Indeed, get_themes() now returns an array of WP_Theme objects, because it
 can. get_theme() returns a WP_Theme object, as does current_theme_info().
 (All are deprecated, as is get_broken_themes().)

 If you don't believe me on backwards compatibility, head over to the Theme
 Editor, which passes around theme names like they're crack. This file is
 the only area that hasn't been updated to rely on WP_Theme; it still uses
 get_themes() and the array it returns for each theme. Everything works.

 == The Parent Theme ==

 A reference to a WP_Theme object for a parent theme is stored in the
 parent property, and accessible with the parent() method. Want a theme's
 parent name? $theme->parent() && $theme->parent()->display('Name').

 == Caching and Performance ==

 Okay, so, this is sweet. After profiling get_themes() and WP_Theme with
 Xdebug and KCacheGrind, a few functions have been sped up, in some cases
 pretty significantly. This includes search_theme_directories() and theme
 searching, which was a drag for no real reason. Searching is now improved
 significantly, and is implemented as a class method to allow for better
 speeds. (Now that things are "ready," I hope to go back and look for
 further improvements.)

 Once sorting was fixed, two things stood out as weighing down processing
 the most: Filesystem reads, and sanitization of headers. With WP_Theme,
 both are non-persistently cached, internally, to ensure serious speed. No
 style.css is ever read twice, and no header is ever sanitized twice. Same
 goes for identifying screenshots and PHP/CSS files — everything gets
 cached.

 Non-persistence is a must for themes because someone might make an FTP
 change then refresh the page. BUT, that doesn't mean a particular
 installation shouldn't be able to opt into persistence.

 When the themes cache bucket is persistent (there's a filter), WP_Theme
 gets smart about it. To prevent thousands of cache sets to update the
 headers cache, it sanitizes every header once the first one is requested
 and adds it to cache. Additionally, search_theme_directories() is cached.

 With persistent caching, it is possible to hit themes.php, and even do a
 search, and avoid a single filesystem or kses operation. The 150-something
 themes in the WP.com themes repository sings in this situation.

 == What's Next? ==

 1. Commit this bad boy.

 2. If object cache drop-ins had a wp_cache_get_non_persistent_groups()
 function, we could scrap that filter and make it much cleaner. (It's a
 rough implementation and could use some tweaks anyway.)

 3. Some of the filesystem functions — specifically, the underlying
 scandir() static method and get_files() — don't quite have their arguments
 or loops ironed out yet. Once the theme editor is converted over, those
 things should come into place nicely. Code review appreciated.

-- 
Ticket URL: <http://core.trac.wordpress.org/ticket/20103#comment:9>
WordPress Trac <http://core.trac.wordpress.org/>
WordPress blogging software


More information about the wp-trac mailing list