Pagepro Blog

Agency Resources

Our WordPress coding standards and best practises

Posted on .

Our WordPress coding standards and best practises

Introduction

General rules

  • Do not build your theme on top of a default WP Theme (Twenty {whatever}). Better pick Underscores or Roots. Building a custom one isn’t a bad idea either.
  • When you’re forced to use a theme, do not modify it either – create a child theme.
  • Be consistent. Apply common formatting rules across all the files in the theme. It’s recommended to use auto-formatting (a feature that is built-in in all modern IDEs). EditorConfig is widely supported – let’s use it. It helps to keep consistency when working in a team.
  • Do not reinvent the wheel. Follow the PSR rules – it’s a widely adopted standard.
  • Name variables in a consistent way: either $camelCase or $underscore_case but never both.
  • Use single and double quotes when appropriate
  • Use meaningful functions names. A good function name should indicate what the function is supposed to do and what type of data it takes and returns.
  • Use meaningful variables names. A good variable name should indicate it’s type.
  • Document the code. If a function name is self-explanatory, then the detailed explanation is not needed but its arguments have to be documented anyway.
  • In order to separate the PHP code from the HTML code, use the alternative control structure syntax.
    // Wrong:
      <?php while (have_posts()) { the_post(); ?>
          <article class="post">
              <?php // Code ?>
          </article>
      <?php } ?>
      
      // Right:
      <?php while (have_posts()) : the_post() ?>
          <article class="post">
              <?php // Code ?>
          </article>
      <?php endwhile; ?>
    
  • Keep templates as clean and logic-less as possible. Move complex logic to separate utility functions.
      // Wrong
      <?php
      $userCompanyId = intval(get_user_meta(get_current_user_id(), 'company-id', true), 10);
      $companyId = intval(get_post_meta(get_the_ID(), 'company_id', true), 10);
      ?>
      
      <?php if ($userCompanyId !== 0 && $userId === $companyId) : ?>
          <div class="company-data">
              <?php // Code ?>
          </div>
      <?php endif; ?>
    
      // Right
      // utils.php
      function my_theme_current_user_can_access()
      {
          $userCompanyId = intval(get_user_meta(get_current_user_id(), 'company-id', true), 10);
          $companyId = intval(get_post_meta(get_the_ID(), 'company_id', true), 10);
      
          if ($userCompanyId !== 0 && $userCompanyId === $companyId) {
              return true;
          }
      
          return false;
      }
      
      // template file
      <?php if (my_theme_current_user_can_access()) : ?>
          <div class="company-data">
              <?php // Code ?>
          </div>
      <?php endif; ?>
    
    
  • Do not echo the HTML tags.
      // Wrong:
      <?php echo '<p class="' . $className . '">' . $content . '</p>'; ?>
    
      // Right:
      <p class="<?php echo $className ?>">
          <?php echo $content; ?>
      </p>
  • Do not use global variables – use namespaces.
  • In order to avoid conflicts, do not use generic names for the utility functions. Remember to prefix custom functions with a unique prefix. get_users() is a wrong function name while my_theme_get_users() is OK.
    Note: Applying this rule is no longer required if a namespace is used, so use a namespace in favor of prefixed functions.
  • It might be a good idea to wrap functions in if (function_exists('function_name')).
    Note: Applying this rule is no longer required if a namespace is used, so use a namespace in favor of prefixed functions.
  • Consider using a template engine like Timber. Do not use a sledgehammer to crack a nut. For a simple theme it’s not required, but a for a complex one it might be a good idea to use such a tool.
  • Use modern PHP syntax like array literals or anonymous functions
      // Wrong:
      add_action('init', 'my_function');
      
      function my_function()
      {
          $myArray = array(1, 2, 3);
          // Do something;
      }
      
      // Right:
      add_action('init', function () {
         $myArray = [1, 2, 3];
         // Do something;
      });
    
  • Use non-anonymous functions only if the function is intended to be removed in the future by remove_action() or remove_filter(). This is the only case when anonymous functions are not recommended.

 

Version Control Setup

  • Store the whole WordPress directory, not only the theme, under version control.
  • Do not store any credentials in the files that are version-controlled. Do not keep /wp-config.php file under version control.
    Note: /wp-config.php might be kept under version control only if the credentials are stored outside the /wp-config.php and not version-controlled.
  • Do not store environment-specific files under version control (e.g. your IDE configuration file).
  • Do not store Media Library directory under version control.
  • Here’s a sample .gitignore file:
      *.log
      /.htaccess
      /license.txt
      /readme.html
      /sitemap.xml
      /sitemap.xml.gz
      wp-config.php
      wp-content/advanced-cache.php
      wp-content/backup-db/
      wp-content/backups/
      wp-content/blogs.dir/
      wp-content/cache/
      wp-content/upgrade/
      wp-content/uploads/
      wp-content/wp-cache-config.php
      wp-content/plugins/hello.php
    

Files Structure

  • Keep the theme root directory clean. The root directory should contain WordPress template files only – this rule applies to the PHP files only. A theme directory may contain build tools’ configuration files (like a gulpfile.jsor a composer.json).
  • Keep partials (the files that are called with a get_template_part() function) under a separate directory (e.g. partials). If there are multiple partials, then create sub-directories to keep the /partials dir well organised.
  • Keep page templates under a separate directory (e.g. page-templates).
  • Keep all the source files (css, js, images, etc) under a separate directory (e.g. src).
  • Keep production-ready files (files compiled with a build tool like Gulp) under a separated directory (e.g. dist). Do not commit the dist directory to the repository.
  • When you use the ACF plugin, remember to create a acf-json directory under the theme directory. Once this directory exists, each time you save a field group, a JSON file will be created / updated.

functions.php

  • Do not put everything in the functions.php file. Split the file into smaller feature-specific partials and save them under a sub-directory (e.g. inc) for example:
      - inc/register-assets.php 
      - inc/custom-post-types.php
      - inc/permissions.php
      - inc/custom-image-sizes.php
    
  • Files included within functions.php should never call WordPress functions directly. Always attach a function to a proper hook.
      // Wrong
      add_image_size('my_custom_size', 200, 200, true);
    
      // Right
      add_action('after_setup_theme', function () {
          add_image_size('my_custom_size', 200, 200, true)
      });
    
  • Files included within functions.php should never call WordPress functions directly. Always attach a function to a proper hook.
      // Wrong
      add_image_size('my_custom_size', 200, 200, true);
    
      // Right
      add_action('after_setup_theme', function () {
          add_image_size('my_custom_size', 200, 200, true)
      });
    
  • Thanks to this, files order doesn’t make any difference.
      // functions.php
      
      // Instead of calling the files in the specific order:
      require_once('inc/register-assets.php'); 
      require_once('inc/custom-post-types.php');
      require_once('inc/permissions.php');
      require_once('inc/custom-image-sizes.php');
      (...)
      
      // The files can be included dynamically:
      call_user_func(function () {
      
          $fileList = [];
          $directoryIterator = new DirectoryIterator(__DIR__ . DIRECTORY_SEPARATOR . 'inc');
      
          foreach ($directoryIterator as $fileInfo) {
      
              if ($fileInfo->isDot()) {
                  continue;
              }
      
              if ($fileInfo->getExtension() === 'php') {
                  $fileList[] = $fileInfo->getRealPath();
              }
      
          }
      
          foreach ($fileList as $file) {
              require_once($file);
          }
      
      });
    
  • Do not put 3rd party libraries under version control. Use Composer and store the dependencies in the composer.json file.

Resources

  • Do not use a wp_head hook to add JS/CSS files. It’s a common practice, but it’s not a proper hook.
  • Use the proper hook instead: wp_enqueue_scripts. The name of the hook might be confusing, but this is the right hook for both, JS and CSS registering.
  • JS/CSS files should be registered first, then enqueued.
  • Use wp_register_script() and wp_register_style()
  • Remember that: wp_register_style() and wp_register_script() have a $ver argument. Always define a version and remember to bump the version number when significant changes are made. It’s important because it forces the clients to download the updated resource version. Otherwise, the browser might grab the resource from the cache. Defining a constant that stores the theme version might be a good idea.
  • Do not overuse scripts/styles registering. Use build tools (like Gulp/Grunt) to concatenate the resources into a single, compressed file. The less HTTP requests a browser makes, the better it is.
  • Even if you use build tools, conditional script enqueuing still might be a good idea. A typical use case is a landing page that has a completely different layout that requires a large amount of additional CSS/JS that is not used on other pages.
      // Example
      add_action('wp_enqueue_scripts', function () {
      
          // Register the `app_js` script
          wp_register_script('app_js', get_template_directory_uri() . "/dist/js/app.js", null, THEME_VER, true);
          
          // Register the `landing_page_js` script
          wp_register_script('landing_page_js', get_template_directory_uri() . "/dist/js/landing-page.js", null, THEME_VER, true);
      
          // Enqueue the `app_js` script on each page
          wp_enqueue_script('app_js');
          
          // Enqueue the `landing_page_js` conditionally
          if (is_page_template('page-templates/landing-page.php')) {
              wp_enqueue_script('landing_page_js');
          }
      
      });

Custom Queries

  • Do not use the query_posts() function.
  • In order to modify the main queries (for example for the archive pages) use the pre_get_posts hook. It’s a more efficient and more elegant solution than calling the get_posts() function or the new WP_query()constructor within a template file.
  • Get familiar with Make Sense of WP Query Functions diagram.
  • Make use of WP_Object_Cache in the case of complex and repeatable queries.
  • When the queries are built based on users’ input, remember to sanitize the data. Use PHP built-in filters or even better – register a custom query var and get its value with get_query_var()
      // Wrong:
      add_action('pre_get_posts', function (\WP_Query $query) {
      
          $query->set('meta_query', [
               [
                   'key' => 'filter',
                   'value' => $_GET['filter'],
               ]
          ]);
      
      });
      
      // Right
      add_filter('query_vars', function ($vars) {
          
          $customVars = [
              'filter',
          ];
      
          foreach ($customVars as $customVar) {
              $vars[] = $customVar;
          }
      
          return $vars;
          
      });
      
      add_action('pre_get_posts', function (\WP_Query $query) {
      
          $query->set('meta_query', [
              [
                  'key' => 'filter',
                  'value' => get_query_var('filter'),
              ]
          ]);
      
      });

AJAX

  • Use a proper hook to register AJAX callbacks: wp_ajax_{action} for the functions to be executed by logged-in users and wp_ajax_nopriv_{action} for the functions to be executed by non-logged-in users.
  • Use nonces.
  • Use wp_localize_sctipt() function in order to expose the AJAX url to the front-end JS scripts.
      // Example
      wp_localize_script('app_js', 'AJAX', [
          'url' => admin_url('admin-ajax.php'),
          'nonce' => wp_create_nonce('ajax_nonce')
      ]);
    
  • Sanitize the data. Use PHP built-in filters – filter_input() and filter_input_array().
  • Do not use die() to send the response back to the client. Use wp_send_json_error() and wp_send_json_success() instead.
      // Example
      add_action('wp_ajax_my_action', function () {
      
          // Nonce validation
          if (!wp_verify_nonce($_POST['nonce'], 'ajax_nonce')) {
              exit(__('No naughty business please', 'my_textdomain'));
          }
      
          // Sanitize the data
          $counter = filter_input(INPUT_POST, 'counter', FILTER_SANITIZE_NUMBER_INT);
      
          // Validate the data
          if ($counter === false || is_null($counter)) {
              // Send the error response back to the client
              wp_send_json_error();
          }
      
          // Do something.
          (...)
      
          // Send the success response back to the client
          wp_send_json_success();
      
      };
    

Internationalization

Plugins

  • Use as few plugins as possible. The more plugins you use, the greater chances there are that something will go wrong during updates.
  • Remember that each plugin increases the security vulnerability. The WordPress core is quite secure (in general), whereas plugins are not (not in general… but still).
  • Do not use plugins for trivial tasks like custom posts registering. Use the generators.
  • Avoid non-maintained, outdated plugins. Check an author’s website, Github, review the issues list, check the response time, etc.
  • Avoid too complex plugins, It’s better to use multiple, tiny tools that do only one thing and do it right instead of big, fully-featured, bloated plugins.
  • Trust nobody. Always review plugins’ code.
  • Try to avoid the plugins that store project-specific data in the database. For instance, Toolset Types stores Custom Post Types setup in the database. It causes problems because the data stored in the database is not version controlled. It’s not always possible, but try to keep this kind of plugins to minimum.
  • Check styles and CSS enqueued by plugins. Sometimes the resources are not needed or are needed only on specific pages. Use wp_dequeue_script and wp_dequeue_style in order to get rid of these files. Use a build tool (like Grunt/Gulp) to concatenate the resources into your CSS/JS files or use wp_enqueue_style() / wp_enqueue_script() to enqueue the files only when they’re required.

Misc

    • Do not make your theme code data-dependent. For example, instead of creating page-contact.php or even worse page-23.php which are either slug-specific or id-specific templates it’s better to create a page template.
    • Do not rely on user roles, they might be changed by plugins. It’s better to rely on capabilities.
    • Remove unnecessary pages from the WP Dashboard.
  // Example
  add_action('admin_menu', function () {
  
      if (current_user_can('manage_options')) {
          return;
      }
  
      $pagesToRemove = [
          'edit.php',
          'tools.php',
          'customize.php',
      ];
  
      $subPagesRomove = [
          'theme-editor.php',
          'themes.php',
          'widgets.php'
      ];
  
      foreach ($pagesToRemove as $page) {
          remove_menu_page($page);
      }
  
      foreach ($subPagesRomove as $page) {
          remove_submenu_page('themes.php', $page);
  
      }
  
  });
  • Remember that even when the pages are removed from the menus they can still be accessed directly. In order to revoke the access, an additional effort is required:
      // Example
      add_action('current_screen', function ($currentScreen) {
          
          if (current_user_can('manage_options')) {
              return;
          }
          
          $disallowedIds = [
              'theme-editor',
              'themes',
              'widgets',
              'tools'
          ];
      
          if (in_array($currentScreen->base, $disallowedIds)) {
              wp_die("Access denied");
          }
      
      });
    
  • Do not grant admin access to the site users. Extend users’ permissions when additional privileges are required.
      // Example
      add_action('admin_init', function () {
          $editorRole = get_role('editor');
          $editorRole->add_cap('edit_theme_options');
      });
    
Chris Lojniewski

Chris Lojniewski

http://pagepro.co

There are no comments.
View Comments (0) ...
Navigation