I’ve written a lot recently on object-oriented programming (OOP) for PHP development. One of the things I’ve tried to stress is that using classes doesn’t make code OOP and it doesn’t always use it better. OOP involves using classes to create reusable objects. Too often we use classes as collections of namespaced functions, missing the point of OOP.
But, that doesn’t mean all code has to be “true OOP.” No one goes to your website or loads your app and says “dang, that’s some true OOP.” They care that it works and is performant. As a developer, these are your concerns as well. But adding testable, reusable, and manageable to that list of attributes is important, and often times OOP helps us get there.
Balancing good software design principles, which most of the time help achieve those goals, with the need to make something work and work well is always a struggle. In this article, I want to discuss cross-cutting concerns, with a few practical examples.
These examples are going to lead to talking about static methods and properties of classes. You may want to check out a recent post I wrote that covered static methods and properties in classes first if this concept is new to you
What Is A Cross-Cutting Concern?
We tend to consider object-oriented code as a vertical hierarchy. One class extends another, building on top of it. But, some tasks extend across multiple class hierarchies, like they are cutting, horizontally across those hierarchies.
These “cross-cutting concerns” apply to multiple parts of an application. The classic example of this is logging. This is the kind of task that needs to be used in many classes. We don’t want to repeat ourselves by cutting and pasting the same logging methods across all classes, so we might be tempted to make one base abstract class that all classes extend, that has logging methods in it. This is a misuse of class extension and gets messy quickly.
Another approach would be to put logging in a function that is not in any class. This might work, but functions lack a lot of the capabilities of classes —visibility, properties, etc. Using a function for a logging system, instead of a class, will lead to a big function and probably globals.
A better approach would be to use a class of all static methods that handles the logging.
Caching is another common cross-cutting concern. For example, WordPress’ object cache is used all over the application. The object cache is tracked in a global variable $wp_object_cache and there are a bunch of functions that work with that variable. Using a global isn’t ideal, but it works to keep track of cached values throughout the application.
An Example: Tracking Removed Hooks
As a practical example, imagine you need to remove all callbacks on a particular hook. You can do this with the function remove_all_filters(). But what if you might need to add them back? Having to rewrite the same add_action/add_filter() calls, including those from plugins and themes you didn’t write is not DRY and will be very hard to maintain.
A better way would be to store a record of the removed actions and then easily put them back, is to use that stored record. This means we need a variable, which I’d like to be a class property, not a global, that will act as this record. Let’s look at two ways to create such a class.
Here is the first, which does not use static methods:
class Remover { /** * Track removed hooks * * @var array */ protected $removed = []; /** * Remove all callbacks on a hook with ability to add back later * * @param string $hook Hook name */ public function remove_all( $hook ){ global $wp_filter; if ( isset( $wp_filter[ $hook ] ) ) { if ( class_exists( 'WP_Hook' ) ) { $all = clone $wp_filter[ $hook ]; }else{ $all = $wp_filter[ $hook ]; } if( ! empty( $all ) ){ $this->removed[ $hook ] = $all; remove_all_filters( $hook ); } } } /** * Add back all callbacks removed from a hook * * Must have used $this->remove_all() to remove * * @param string $hook Hook name */ public function re_add( $hook ){ if( isset( $this->removed[ $hook ] ) ){ $hooked = $this->removed[ $hook ]; if ( ! empty( $hooked ) ) { if ( class_exists( 'WP_Hook' ) ) { $priorities = $hooked->callbacks; } else { $priorities = $hooked; } foreach ( $priorities as $priority => $callbacks ) { foreach ( $callbacks as $callback ) { add_filter( $hook, $callback[ 'function' ], $priority, $callback[ 'accepted_args' ] ); } } } } } }
In this case, we would have to create an instance of this object and then keep track of it throughout the application. This is vital because each instance of this object would have different removed hooks in its $removed property.
That might be good. You could supply an instance of this class to any class that needed to remove hooks with the option to put them back later.
That said, there is not a great advantage to that approach, which would require multiple instances of the same class existing throughout the application. In fact, it’s less useful than just having one instance. Before you go reaching for a global variable, or the singleton pattern, let’s look at this class rewritten using static methods and a static property:
<?php class Remover { /** * Track removed hooks * * @var array */ protected static $removed = []; /** * Remove all callbacks on a hook with ability to add back later * * @param string $hook Hook name */ public static function remove_all( $hook ){ global $wp_filter; if ( isset( $wp_filter[ $hook ] ) ) { if ( class_exists( 'WP_Hook' ) ) { $all = clone $wp_filter[ $hook ]; }else{ $all = $wp_filter[ $hook ]; } if( ! empty( $all ) ){ self::$removed[ $hook ] = $all; remove_all_filters( $hook ); } } } /** * Add back all callbacks removed from a hook * * Must have used $this->remove_all() to remove * * @param string $hook Hook name */ public static function re_add( $hook ){ if( isset( self::$removed[ $hook ] ) ){ $hooked = self::$removed[ $hook ]; if ( ! empty( $hooked ) ) { if ( class_exists( 'WP_Hook' ) ) { $priorities = $hooked->callbacks; } else { $priorities = $hooked; } foreach ( $priorities as $priority => $callbacks ) { foreach ( $callbacks as $callback ) { add_filter( $hook, $callback[ 'function' ], $priority, $callback[ 'accepted_args' ] ); } } } } } }
The important thing to remember here about static methods and properties is that they can be accessed without instantiating the class. That means that when we use Remover::remove_all() which uses the static $removed property, we don’t need to worry about tracking instances and when we come back to use Remover::re_add() later, it will be looking in the same $removed property for the tracked hooks.
Now, I have an essentially global system for tracking my removal of hooks, without using a global variable. I don’t have to worry about collisions with other plugins or themes, that use the same named global in some other way, or misuse of the $removed property, since access to it is controlled by my class.
Solve Problems
In this article, I’ve introduced the concept of cross-cutting concerns. I’ve also talked about classes with all static methods as a solution for cross-cutting concerns that require more than a simple function. Just remember the goal is not to adhere to principles for the sake of principles, it is to solve problems, without creating unnecessary technical debt.
I hope this will increase your ability to architect object-oriented PHP code in large projects. For me, dealing with these problems that need to be solved in many different places in a project, has been a challenge. Trying to keep my classes abiding by the single-responsibility principle while avoiding repeating myself was always a challenge. Becoming aware of the concept of cross-cutting concerns helped me understand the problem and see new, more useful solutions.
No Comments