7 Tips to Avoid Spaghetti Code in WordPress (and PHP)

PHP is infamous to be a messy language. And WordPress runs on PHP which inherits the same problem. Let's learn how to avoid that

PHP is infamous to be a messy language in the developer world. That’s because it allows bad code to run.

And WordPress runs on PHP which inherits the same problem.

Let’s take a look at several tips to avoid that:

Tip #1: Group Up Filters & Actions

The snippet below is a common mistake I see in themes and plugins. You can’t see what this file does unless you scroll until the very end.

add_action( 'after_setup_theme', 'my_theme_supports' );
function my_theme_supports() {
  // ...
}

add_action( 'wp_enqueue_scripts', 'my_public_assets', 99 );
function my_public_assets() {
  // ...
}

add_action( 'widgets_init', 'my_register_sidebar' );
function my_register_sidebar() {
  // ...
}

This is fixed simply by grouping the add_action and add_filter in one place:

add_action( 'after_setup_theme', 'my_theme_supports' );
add_action( 'wp_enqueue_scripts', 'my_public_assets', 99 );
add_action( 'widgets_init', 'my_register_sidebar' );


function my_theme_supports() {
  // ...
}

function my_public_assets() {
  // ...
}

function my_register_sidebar() {
  // ...
}

Now you only need to read the top part to see everything that file does.

Tip #2: Use Variable for Long Expression

For example, you want to check if a visitor is using Internet Explorer. You might create a function like this:

function is_browser_ie() {
  // is IE10 or IE11
  return preg_match('~MSIE|Internet Explorer~i', $_SERVER['HTTP_USER_AGENT']) ||
    preg_match('~Trident/7.0(; Touch)?; rv:11.0~',$_SERVER['HTTP_USER_AGENT']);
}

There’s nothing wrong with that, but it’s clearer if you write it like this:

function is_browser_ie() {
  $is_ie10 = preg_match('~MSIE|Internet Explorer~i', $_SERVER['HTTP_USER_AGENT']);
  $is_ie11 = preg_match('~Trident/7.0(; Touch)?; rv:11.0~',$_SERVER['HTTP_USER_AGENT']);

  return $is_ie10 || $is_ie11;
}

The general idea is to use fewer inline comments. Because a lot of inline comments actually made your code harder to read.

Tip #3: Conditional Return Early

Let’s say you want to register a new Gutenberg style. You must first check whether Gutenberg is enabled or not. So you might do this:

add_action( 'init', my_register_block_styles );

function my_register_block_styles() {
  // if Gutenberg is active
  if ( function_exists( 'register_block_type' ) ) {
    register_block_style( 'core/heading', [
      'name' => 'has-underline',
      'label' => 'Has Underline'
    ] );

    register_block_style( 'core/buttons', [
      'name' => 'transparent',
      'label' => 'Transparent'
    ] );
  }
}

Seems good? Yes, but there’s a better way:

add_action( 'init', my_register_block_styles );

function my_register_block_styles() {
  // if Gutenberg is not active
  if ( !function_exists( 'register_block_type' ) ) { return; }
  
  register_block_style( 'core/heading', [
    'name' => 'has-underline',
    'label' => 'Has Underline'
  ] );

  register_block_style( 'core/buttons', [
    'name' => 'transparent',
    'label' => 'Transparent'
  ] );
}

You break the function early when the condition is not met. This way you keep the indentation as low as possible, making it easier to read.

Tip #4: Split functions.php into Several Files

Your functions file can easily grow big. It is best if you split the related chunk of codes into its own file.

It’s up to you how you organize it. My personal preference is to create a folder named functions/ and split them like this:

  • admin.php – Hooks that affect the admin panel.
  • frontend.php – Hooks that affect the front-end site.
  • helpers.php – Reusable utility functions.
  • setup.php – Hooks that initialize the site. It includes: theme support, custom post type, enqueue assets, register widget.
  • timber.php – If you’re using Timber plugin. Contains the global variable and custom filter (Learn How »)
  • shop.php – If you’re using WooCommerce.

Then your functions.php only acts as a gateway:

$function_dir = __DIR__ . '/functions';

require_once $function_dir . '/helpers.php';
require_once $function_dir . '/setup.php';

if( is_admin() ) {
  require_once $function_dir . '/admin.php';
} else {
  require_once $function_dir . '/frontend.php';
  require_once $function_dir . '/timber.php';
}

if( class_exists('WooCommerce') ) {
  require_once $function_dir . '/shop.php';
}

Tip #5: Utilize WordPress Native Functions

I often see themes and plugins re-create a function that is already provided by WordPress. Most likely because they’re not aware of them.

So I recommend scanning through the list of functions here codex.wordpress.org/Function_Reference

One day when you need it, you will vaguely remember that there’s a function for that. Then you can find it with Google search.

Below are some useful native WP functions:

Tip #6: Define Available Arguments

If you create a function that accepts arguments, define the available one with wp_parse_args. This will also set the default value.

For example, take a look at the native get_posts() function:

function get_posts( $args = null ) {
  
  $parsed_args = wp_parse_args( $args, [
    'numberposts'      => 5,
    'category'         => 0,
    'orderby'          => 'date',
    'order'            => 'DESC',
    'include'          => [],
    'exclude'          => [],
    'meta_key'         => '',
    'meta_value'       => '',
    'post_type'        => 'post',
    'suppress_filters' => true,
  ] );

  // ...
}

Having all the possible arguments laid out like that will make it easy for another developer to know what to pass on. It’s also a good reminder for you.

Tip #7: Separate Template Into Its Own File

Mixing logic and template code will lead to messy code. That’s just how it is.

For example, we are making a function to display Related Posts like this:

<?php
function output_related_posts() {
  global $post;

  // get categories
  $category = get_the_category( $post->ID );
  $category_ids = [];
  foreach( $category as $c ) {
    $category_ids[] = $c->term_id;
  }

  // get posts with same categories, randomly
  $posts = get_posts([
    'category' => $category_ids,
    'post_type' => 'post',
    'posts_per_page' => 3,
    'orderby' => 'rand',
    'post__not_in' => [$post->ID]
  ]);

  ?>
  <ul class="recent-posts">
  <?php foreach( $posts as $p ): ?>
    
    <li class="post-thumbnail">
      <a href="<?php echo get_permalink( $p ); ?>">
        <?php echo get_the_post_thumbnail( $p, 'thumbnail' ); ?>
        <h3><?php echo $p->post_title; ?></h3>
        <time><?php echo get_the_date( '', $p ); ?></time>
        <p><?php echo $p->post_excerpt; ?></p>
      </a>
    </li>

  <?php endforeach; ?>
  </ul>
  <?php
}

That coding style is common in PHP. But if you have ventured to other programming languages, you will notice how dirty it actually is.

One way to fix this is to split the template into its own file and use get_template_part() like shown below:

<?php
function output_related_posts() {
  global $post;

  // get categories
  $category = get_the_category( $post->ID );
  $category_ids = [];
  foreach( $category as $c ) {
    $category_ids[] = $c->term_id;
  }

  // get posts with same categories, randomly
  $posts = get_posts([
    'category' => $category_ids,
    'post_type' => 'post',
    'posts_per_page' => 3,
    'orderby' => 'rand',
    'post__not_in' => [$post->ID]
  ]);

  get_template_part( 'views/related-posts', '', [
    'posts' => $posts
  ] );
}
<ul class="recent-posts">
<?php foreach( $args['posts'] as $p ): ?>
  
  <li class="post-thumbnail">
    <a href="<?php echo get_permalink( $p ); ?>">
      <?php echo get_the_post_thumbnail( $p, 'thumbnail' ); ?>
      <h3><?php echo $p->post_title; ?></h3>
      <time><?php echo get_the_date( '', $p ); ?></time>
      <p><?php echo $p->post_excerpt; ?></p>
    </a>
  </li>

<?php endforeach; ?>
</ul>

Note: This only works in WordPress version 5.5 and above.


Conclusion

There are a lot of themes and plugins with messy codes out there. We can’t control them, but at the very least our code shouldn’t fall into the same pit hole.

After this, I suggest reading phptherightway.com. It has a complete collection of popular PHP coding standards.

Do you have a best practice that you want to share? Feel free to share it in the comment section below 🙂

Default image
Henner Setyono
A web developer who mainly build custom theme for WordPress since 2013. Young enough to never had to deal with using Table as layout.
Leave a Reply

2 Comments

  1. Hey, i disagree with you on the last example. The rewrite mixes BUSINESS LOGIC and VIEW LOGIC, which is perfectly separated in the non-rewrite version. PHP is a templating language as you know, so i recommend not to rewrite the last example.

    • Hi Thomas, Thanks for your insight. I agree with you about not mixing Business and View logic.

      But after thinking about it, the example before the rewrite also shares the same problem because it mixes everything in a function.

      One thing that I learned recently is that WP 5.5 introduces variable passing to get_template_part(). So we can truly separate the View into its own file.

      I will update the last example into "Do Not Mix View Logic" or a similar title.