Permalink
Browse files

Introduce Abstract_Admin_Page class.

  • Loading branch information...
felixarntz committed Jan 14, 2018
1 parent 8f4f979 commit f6ef1da22f4ab20a79192f5b3b88970aba561a6a
Showing with 138 additions and 1 deletion.
  1. +2 −1 composer.json
  2. +116 −0 src/Admin_Page/Abstract_Admin_Page.php
  3. +20 −0 src/Exception/Config_Invalid_Exception.php
@@ -27,7 +27,8 @@
}
},
"require": {
"php": ">=7.0"
"php": ">=7.0",
"brightnucleus/config": "~0.4"

This comment has been minimized.

@felixarntz

felixarntz Jan 19, 2018

Owner

We're gonna use a third-party library for the configuration handling so that we don't need to reinvent the wheel. Try to use existing libraries wherever possible. This particular library we use is maintained by @schlessera.

},
"require-dev": {
"codeclimate/php-test-reporter": "dev-master",
@@ -0,0 +1,116 @@
<?php
/**
* Abstract_Admin_Page class
*
* @package Leaves_And_Love\OOP_Admin_Pages\Admin_Page
* @since 1.0.0
*/
namespace Leaves_And_Love\OOP_Admin_Pages\Admin_Page;
use Leaves_And_Love\OOP_Admin_Pages\Admin_Page;
use Leaves_And_Love\OOP_Admin_Pages\Exception\Config_Invalid_Exception;
use BrightNucleus\Config\ConfigInterface as Config;
use BrightNucleus\Config\ConfigTrait;

This comment has been minimized.

@felixarntz

felixarntz Jan 19, 2018

Owner

If you use something in a file that is part of another namespace, you need to "import" it at the top of the file, right below the namespace declaration. You can even provide an alias to use, as seen in line 13.

/**
* Abstract class representing an admin page.
*
* @since 1.0.0
*/
abstract class Abstract_Admin_Page implements Admin_Page {
use ConfigTrait;

This comment has been minimized.

@felixarntz

felixarntz Jan 19, 2018

Owner

Traits (available since PHP 5.4) are a mechanism for reusing code without relying on abstract classes and inheritance. When a class uses a trait, its methods and properties are "acquired" by the class as if they were directly part of it.

const SLUG = 'slug';
const TITLE = 'title';
const RENDER_CALLBACK = 'render_callback';
const INITIALIZE_CALLBACK = 'initialize_callback';

This comment has been minimized.

@felixarntz

felixarntz Jan 19, 2018

Owner

One should not rely on strings as identifiers as typos easily happen. By providing class constants that other developers can use instead, they will know immediately when they made a typo, as it won't even be possible to run the code in that case. When using strings, the code would still run, but not work, and you would possibly need to debug for quite a bit, just in order to find a simple typo.

/**
* Constructor.
*
* Sets the configuration.
*
* @since 1.0.0
*
* @param Config $config Admin page configuration. Must contain keys 'slug' (admin page identifier), 'title'
* and 'render_callback' (callable that receives the configuration as sole parameter).
* May also contain an 'initialize_callback' key.
*
* @throws Config_Invalid_Exception Thrown when the configuration is invalid.
*/
public function __construct( Config $config ) {
$this->processConfig( $config );

This comment has been minimized.

@felixarntz

felixarntz Jan 19, 2018

Owner

This is a protected method made available by the ConfigTrait. Some other methods used here are as well, such as getConfigKey() and hasConfigKey().

$required_keys = $this->get_required_config_keys();
foreach ( $required_keys as $required_key ) {
if ( ! $this->hasConfigKey( $required_key ) ) {
throw new Config_Invalid_Exception( sprintf( 'The required configuration key %s is missing.', $required_key ) );
}
$value = $this->getConfigKey( $required_key );
if ( empty( $value ) ) {
throw new Config_Invalid_Exception( sprintf( 'The required configuration key %s is empty.', $required_key ) );
}
}
}
/**
* Gets the slug of the admin page.
*
* @since 1.0.0
*
* @return string Admin page slug.
*/
public function get_slug() : string {
return $this->getConfigKey( self::SLUG );
}
/**
* Gets the title of the admin page.
*
* @since 1.0.0
*
* @return string Admin page title.
*/
public function get_title() : string {
return $this->getConfigKey( self::TITLE );
}
/**
* Initializes the admin page on pageload.
*
* This method must be called before any output is printed.
*
* @since 1.0.0
*/
public function initialize() {
if ( ! $this->hasConfigKey( self::INITIALIZE_CALLBACK ) ) {
return;
}
call_user_func( $this->getConfigKey( self::INITIALIZE_CALLBACK ), $this->config );
}
/**
* Renders the admin page content.
*
* @since 1.0.0
*/
public function render() {
call_user_func( $this->getConfigKey( self::RENDER_CALLBACK ), $this->config );
}
/**
* Gets the configuration keys that are required for an admin page.
*
* @since 1.0.0
*
* @return array Array of configuration keys.
*/
protected function get_required_config_keys() {
return array( self::SLUG, self::TITLE, self::RENDER_CALLBACK );
}
}
@@ -0,0 +1,20 @@
<?php
/**
* Config_Invalid_Exception class
*
* @package Leaves_And_Love\OOP_Admin_Pages\Exception
* @since 1.0.0
*/
namespace Leaves_And_Love\OOP_Admin_Pages\Exception;
use Exception;
/**
* Exception thrown when a configuration is invalid.
*
* @since 1.0.0
*/
class Config_Invalid_Exception extends Exception {
}

1 comment on commit f6ef1da

@felixarntz

This comment has been minimized.

Owner

felixarntz commented on f6ef1da Jan 19, 2018

Why an abstract class and an interface?

Instead of having an abstract class implementing our Admin_Page interface, we might as well have written a WordPress-specific implementation. However, since much of the code in it would be so basic that it could even be used in other implementations, having an additional base class to extend makes it easy to create more specific implementations without ending up with duplicate code.

What is this Config good for?

Something that is unfortunately very common in WordPress is the so-called $args-pattern (please do not consider this when learning about design patterns). An array of arguments is simply an unstructured dump of data without any way to validate internally or hint against specific implementations. A configuration object represents a better version of it. Since it extends PHP's ArrayObject class, the configuration values can be accessed as if it was an array. In addition though, it supports further methods that enable for example extracting out a subconfiguration, validating the configuration, or merging it with another one.

Why throw an exception? And why even a custom exception?

While WordPress usually returns WP_Error objects or throws _doing_it_wrong() notices, pretty much everywhere outside of that space an exception is thrown in case of issues. When something isn't used as expected, don't try to be clever and hack around it some way. Let the developer know that they did something wrong. Exceptions exist for this very reason and they will stop execution of any further code, unless someone basically "expects" to possibly run into that exception and thus wraps it in a "try-catch" block. Furthermore, it is a best practice to use specific exception classes for specific types of exceptions so that "catch" blocks can target specific exceptions that may be thrown.

Please sign in to comment.