Basic release
This commit is contained in:
		@@ -0,0 +1,7 @@
 | 
			
		||||
<phpunit boostrap="tests/bootstrap.php">
 | 
			
		||||
  <testsuites>
 | 
			
		||||
    <testsuite>
 | 
			
		||||
      <directory suffix="Test.php" phpVersion="5.4.0">tests</directory>
 | 
			
		||||
    </testsuite>
 | 
			
		||||
  </testsuites>
 | 
			
		||||
</phpunit>
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A composite option with a fixed number of suboptions and their default values.
 | 
			
		||||
 */
 | 
			
		||||
abstract class CompositeOption implements \ArrayAccess, \Countable, \IteratorAggregate
 | 
			
		||||
{
 | 
			
		||||
	use HookableTrait;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @var string
 | 
			
		||||
	 */
 | 
			
		||||
	protected $slug;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return string
 | 
			
		||||
	 */
 | 
			
		||||
	public function getSlug()
 | 
			
		||||
	{
 | 
			
		||||
		return $this->slug;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	protected function __construct( $slug )
 | 
			
		||||
	{
 | 
			
		||||
		$this->slug = (string) $slug;
 | 
			
		||||
 | 
			
		||||
		$this->addHookMethod( 'default_option_' . $this->slug, 'getDefaults' );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return default values for all suboptions.
 | 
			
		||||
	 * Hooked into get_option() defaults.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return array
 | 
			
		||||
	 */
 | 
			
		||||
	abstract public function getDefaults();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param string $offset
 | 
			
		||||
	 * @return mixed
 | 
			
		||||
	 */
 | 
			
		||||
	public function getDefault( $offset )
 | 
			
		||||
	{
 | 
			
		||||
		return $this->getDefaults()[ $offset ];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function offsetExists( $offset )
 | 
			
		||||
	{
 | 
			
		||||
		$settings = get_option( $this->slug );
 | 
			
		||||
		return isset( $settings[ $offset ] );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetGet( $offset )
 | 
			
		||||
	{
 | 
			
		||||
		$settings = get_option( $this->slug );
 | 
			
		||||
		if ( ! isset( $settings[ $offset ] ) ) {
 | 
			
		||||
			throw new \OutOfBoundsException( "There is no setting '$offset'." );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return $settings[ $offset ];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetSet( $offset, $value )
 | 
			
		||||
	{
 | 
			
		||||
		$settings = get_option( $this->slug );
 | 
			
		||||
		if ( ! isset( $settings[ $offset ] ) ) {
 | 
			
		||||
			throw new \OutOfBoundsException( "There is no setting '$offset'." );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		$settings[ $offset ] = $value;
 | 
			
		||||
		update_option( $this->slug, $settings );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetUnset( $offset )
 | 
			
		||||
	{
 | 
			
		||||
		throw new \BadMethodCallException( 'You cannot unset settings.' );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function count()
 | 
			
		||||
	{
 | 
			
		||||
		return count( $this->getDefaults() );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function getIterator()
 | 
			
		||||
	{
 | 
			
		||||
		return new \ArrayIterator( get_option( $this->slug ) );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Making actions and filters how they should be.
 | 
			
		||||
 * 
 | 
			
		||||
 * Default to pass all arguments.
 | 
			
		||||
 */
 | 
			
		||||
trait HookableTrait
 | 
			
		||||
{
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param string $action_or_filter
 | 
			
		||||
	 * @param string $method
 | 
			
		||||
	 * @param int    $priority
 | 
			
		||||
	 * @param int    $acceptedArgs
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addHookMethod( $action_or_filter, $method, $priority = 10, $acceptedArgs = 99 )
 | 
			
		||||
	{
 | 
			
		||||
		add_filter( $action_or_filter, array( $this, $method ), $priority, $acceptedArgs );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param string $action_or_filter
 | 
			
		||||
	 * @param string $method
 | 
			
		||||
	 * @param int    $priority
 | 
			
		||||
	 * @param int    $acceptedArgs
 | 
			
		||||
	 */
 | 
			
		||||
	protected function removeHookMethod( $action_or_filter, $method, $priority = 10, $acceptedArgs = 99 )
 | 
			
		||||
	{
 | 
			
		||||
		remove_filter( $action_or_filter, array( $this, $method ), $priority, $acceptedArgs );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options wrapper.
 | 
			
		||||
 */
 | 
			
		||||
class Options implements \ArrayAccess
 | 
			
		||||
{
 | 
			
		||||
	/**
 | 
			
		||||
	 * Remember a default value.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * Creates a closure and hooks it into the 'default_option_*' filter.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string $option
 | 
			
		||||
	 * @param mixed  $value
 | 
			
		||||
	 */
 | 
			
		||||
	public function setDefault( $option, $value )
 | 
			
		||||
	{
 | 
			
		||||
		add_filter( 'default_option_' . $option, function () use ( $value ) {
 | 
			
		||||
			return $value;
 | 
			
		||||
		} );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function offsetExists( $option )
 | 
			
		||||
	{
 | 
			
		||||
		// Unfortunately, we can’t really tell whether it exists…
 | 
			
		||||
		$value = get_option( $option );
 | 
			
		||||
		return false !== $value && null !== $value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetGet( $option )
 | 
			
		||||
	{
 | 
			
		||||
		return get_option( $option );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetSet( $option, $value )
 | 
			
		||||
	{
 | 
			
		||||
		update_option( $option, $value );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetUnset( $option )
 | 
			
		||||
	{
 | 
			
		||||
		delete_option( $option );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Marker class for WordPress plugins.
 | 
			
		||||
 */
 | 
			
		||||
abstract class Plugin
 | 
			
		||||
{
 | 
			
		||||
	use HookableTrait;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,378 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstracts common functionality and escaping for the Settings API.
 | 
			
		||||
 * 
 | 
			
		||||
 * TODO: Check slug and field names for illegal characters.
 | 
			
		||||
 * TODO: Refactor to not extend but use CompositeOption
 | 
			
		||||
 */
 | 
			
		||||
abstract class SettingsPage extends CompositeOption
 | 
			
		||||
{
 | 
			
		||||
	use HookableTrait;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @var string
 | 
			
		||||
	 */
 | 
			
		||||
	protected $capability;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Slug (file name) of the parent menu entry.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @see add_submenu_page() for suggestions.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return string|null
 | 
			
		||||
	 */
 | 
			
		||||
	protected function getParentSlug()
 | 
			
		||||
	{
 | 
			
		||||
		return 'options-general.php';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Title for this setting page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return string
 | 
			
		||||
	 */
 | 
			
		||||
	abstract protected function getPageTitle();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * String to show in the menu entry.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return string
 | 
			
		||||
	 */
 | 
			
		||||
	protected function getMenuTitle()
 | 
			
		||||
	{
 | 
			
		||||
		return $this->getPageTitle();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param string $slug       Page slug
 | 
			
		||||
	 * @param string $capability Required capability to view
 | 
			
		||||
	 */
 | 
			
		||||
	protected function __construct( $slug, $capability = 'manage_options' )
 | 
			
		||||
	{
 | 
			
		||||
		parent::__construct( $slug );
 | 
			
		||||
 | 
			
		||||
		$this->capability = (string) $capability;
 | 
			
		||||
 | 
			
		||||
		$this->addHookMethod( 'admin_menu', 'registerPage' );
 | 
			
		||||
		$this->addHookMethod( 'admin_init', 'registerSettings' );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Register our options page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return void
 | 
			
		||||
	 */
 | 
			
		||||
	public function registerPage()
 | 
			
		||||
	{
 | 
			
		||||
		add_submenu_page(
 | 
			
		||||
			$this->getParentSlug(),
 | 
			
		||||
			$this->getPageTitle(),
 | 
			
		||||
			$this->getMenuTitle(),
 | 
			
		||||
			$this->capability,
 | 
			
		||||
			$this->slug,
 | 
			
		||||
			array( $this, 'renderPage' )
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Register the actual settings.
 | 
			
		||||
	 * Override and use addSection() and add*Field() methods.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return void
 | 
			
		||||
	 */
 | 
			
		||||
	public function registerSettings()
 | 
			
		||||
	{
 | 
			
		||||
		register_setting(
 | 
			
		||||
			$this->slug,
 | 
			
		||||
			$this->slug,
 | 
			
		||||
			array( $this, 'sanitizeOptions' )
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sanitize option values after form submission.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param array $options
 | 
			
		||||
	 * @return array
 | 
			
		||||
	 */
 | 
			
		||||
	abstract public function sanitizeOptions( array $options );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Section to add fields to.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * Parameter default from add_settings_field().
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @var string
 | 
			
		||||
	 */
 | 
			
		||||
	protected $currentSection = 'default';
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add a new section and return its generated name.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string $title optional, can be empty
 | 
			
		||||
	 * @param string $name  optional, will be generated if empty
 | 
			
		||||
	 * @return string
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addSection( $title = '', $name = '' )
 | 
			
		||||
	{
 | 
			
		||||
		$name = (string) $name;
 | 
			
		||||
		if ( ! strlen( $name ) ) {
 | 
			
		||||
			$name = $this->slug . '-section' . ( count( $this->getSections() ) + 1 );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		add_settings_section(
 | 
			
		||||
			$name,
 | 
			
		||||
			esc_html( $title ),
 | 
			
		||||
			array( $this, 'renderSectionTeaser' ),
 | 
			
		||||
			$this->slug
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		return $this->currentSection = $name;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Callback before output of section fields.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * Teasers must escape their output themselves.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param array $section {
 | 
			
		||||
	 *    @type string   $id
 | 
			
		||||
	 *    @type string   $title
 | 
			
		||||
	 *    @type callable $callback
 | 
			
		||||
	 * }
 | 
			
		||||
	 */
 | 
			
		||||
	public function renderSectionTeaser( array $section )
 | 
			
		||||
	{
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Get all known section names on this page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @global $wp_settings_fields
 | 
			
		||||
	 * @return array<string>
 | 
			
		||||
	 */
 | 
			
		||||
	public function getSections()
 | 
			
		||||
	{
 | 
			
		||||
		global $wp_settings_fields;
 | 
			
		||||
 | 
			
		||||
		if ( ! isset( $wp_settings_fields[ $this->slug ] ) ) {
 | 
			
		||||
			return array();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return array_keys( $wp_settings_fields[ $this->slug ] );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @global $wp_settings_fields
 | 
			
		||||
	 * @param string $section
 | 
			
		||||
	 * @return array<string, array {
 | 
			
		||||
	 *    @type string   $id
 | 
			
		||||
	 *    @type string   $title
 | 
			
		||||
	 *    @type callable $callback
 | 
			
		||||
	 *    @type array    $args
 | 
			
		||||
	 * }>
 | 
			
		||||
	 */
 | 
			
		||||
	public function getFieldsForSection( $section )
 | 
			
		||||
	{
 | 
			
		||||
		global $wp_settings_fields;
 | 
			
		||||
 | 
			
		||||
		return $wp_settings_fields[ $this->slug ][ $section ];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add a custom field to this setting page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string   $name
 | 
			
		||||
	 * @param string   $label
 | 
			
		||||
	 * @param callable $callback
 | 
			
		||||
	 * @param array    $args
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addField( $name, $label, $callback, $args = array() )
 | 
			
		||||
	{
 | 
			
		||||
		if ( ! is_callable( $callback ) ) {
 | 
			
		||||
			throw new \InvalidArgumentException( 'Parameter $callback must be callable.' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		add_settings_field(
 | 
			
		||||
			$name,
 | 
			
		||||
			esc_html( $label ),
 | 
			
		||||
			$callback,
 | 
			
		||||
			$this->slug,
 | 
			
		||||
			$this->currentSection,
 | 
			
		||||
			$args
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add a text field to this settings page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string $name
 | 
			
		||||
	 * @param string $label
 | 
			
		||||
	 * @param array  $args
 | 
			
		||||
	 * @param string $callback
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addTextField( $name, $label, $args = array(), $callback = '' )
 | 
			
		||||
	{
 | 
			
		||||
		if ( ! $callback || ! is_callable( $callback ) ) {
 | 
			
		||||
			$callback = array( $this, 'renderTextField' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		$this->addField(
 | 
			
		||||
			$name,
 | 
			
		||||
			$label,
 | 
			
		||||
			$callback,
 | 
			
		||||
			$args + array(
 | 
			
		||||
				'label_for' => $this->slug . '-' . $name,
 | 
			
		||||
				'name'      => $name,
 | 
			
		||||
				'attr'      => array(),
 | 
			
		||||
			)
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param string   $name
 | 
			
		||||
	 * @param string   $label
 | 
			
		||||
	 * @param string   $caption
 | 
			
		||||
	 * @param array    $args
 | 
			
		||||
	 * @param callable $callback
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addCheckboxField( $name, $label, $caption, $args = array(), $callback = '' )
 | 
			
		||||
	{
 | 
			
		||||
		if ( ! $callback || ! is_callable( $callback ) ) {
 | 
			
		||||
			$callback = array( $this, 'renderCheckboxField' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		$this->addField(
 | 
			
		||||
			$name,
 | 
			
		||||
			$label,
 | 
			
		||||
			$callback,
 | 
			
		||||
			$args + array(
 | 
			
		||||
				'label_for' => $this->slug . '-' . $name,
 | 
			
		||||
				'name'      => $name,
 | 
			
		||||
				'caption'   => $caption,
 | 
			
		||||
				'attr'      => array(),
 | 
			
		||||
			)
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add an error.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string $code
 | 
			
		||||
	 * @param string $message
 | 
			
		||||
	 */
 | 
			
		||||
	protected function addError( $code, $message )
 | 
			
		||||
	{
 | 
			
		||||
		add_settings_error( $this->slug, $code, $message );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Errors for this setting.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @return array {
 | 
			
		||||
	 *    @type string $setting
 | 
			
		||||
	 *    @type string $code
 | 
			
		||||
	 *    @type string $message
 | 
			
		||||
	 *    @type string $type 'error'
 | 
			
		||||
	 * }
 | 
			
		||||
	 */
 | 
			
		||||
	public function getErrors()
 | 
			
		||||
	{
 | 
			
		||||
		return get_settings_errors( $this->slug );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Render a setting as text field.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param array $args {
 | 
			
		||||
	 *    @type string $name
 | 
			
		||||
	 *    @type string $label_for
 | 
			
		||||
	 *    @type array  $attr
 | 
			
		||||
	 * }
 | 
			
		||||
	 */
 | 
			
		||||
	public function renderTextField( array $args )
 | 
			
		||||
	{
 | 
			
		||||
		$name = $args['name'];
 | 
			
		||||
 | 
			
		||||
		$this->renderInputTag( array(
 | 
			
		||||
			'type'  => 'text',
 | 
			
		||||
			'id'    => $args['label_for'],
 | 
			
		||||
			'class' => 'regular-text',
 | 
			
		||||
			'name'  => $this->slug . '[' . $name . ']',
 | 
			
		||||
			'value' => $this[ $name ],
 | 
			
		||||
		) + $args['attr'] );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Render a setting as checkbox.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param array $args {
 | 
			
		||||
	 *    @type string $caption
 | 
			
		||||
	 *    @type string $name
 | 
			
		||||
	 *    @type string $label_for
 | 
			
		||||
	 *    @type array  $attr
 | 
			
		||||
	 * }
 | 
			
		||||
	 */
 | 
			
		||||
	public function renderCheckboxField( array $args )
 | 
			
		||||
	{
 | 
			
		||||
		$name = $args['name'];
 | 
			
		||||
 | 
			
		||||
		echo '<label>';
 | 
			
		||||
		$this->renderInputTag( array(
 | 
			
		||||
			'type'    => 'checkbox',
 | 
			
		||||
			'id'      => isset( $args['label_for'] ) ? $args['label_for'] : null,
 | 
			
		||||
			'name'    => $this->slug . '[' . $name . ']',
 | 
			
		||||
			'value'   => '1',
 | 
			
		||||
			'checked' => $this[ $name ] ? 'checked' : null,
 | 
			
		||||
		) + $args['attr'] );
 | 
			
		||||
		echo ' ' . esc_html( $args['caption'] ) . '</label>';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Output an input tag with given HTML attributes.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param array $attr
 | 
			
		||||
	 */
 | 
			
		||||
	protected function renderInputTag( array $attr )
 | 
			
		||||
	{
 | 
			
		||||
		echo '<input';
 | 
			
		||||
		foreach ( $attr as $k => $v ) {
 | 
			
		||||
			if ( null !== $v ) {
 | 
			
		||||
				printf( ' %s="%s"', $k, esc_attr( $v ) );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		echo ' />';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Output settings page.
 | 
			
		||||
	 */
 | 
			
		||||
	public function renderPage()
 | 
			
		||||
	{
 | 
			
		||||
		if ( ! current_user_can( $this->capability ) ) {
 | 
			
		||||
			wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		?>
 | 
			
		||||
		<div class="wrap">
 | 
			
		||||
			<h2><?php echo esc_html( $this->getPageTitle() ); ?></h2>
 | 
			
		||||
			<form action="options.php" method="post">
 | 
			
		||||
				<?php settings_errors( 'general' ); // “Settings saved.” message ?>
 | 
			
		||||
				<?php settings_fields( $this->slug ); ?>
 | 
			
		||||
				<?php do_settings_sections( $this->slug ); ?>
 | 
			
		||||
				<?php submit_button(); ?>
 | 
			
		||||
			</form>
 | 
			
		||||
		</div>
 | 
			
		||||
		<?php
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Site options wrapper.
 | 
			
		||||
 */
 | 
			
		||||
class SiteOptions implements \ArrayAccess
 | 
			
		||||
{
 | 
			
		||||
	/**
 | 
			
		||||
	 * Remember a default value.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * Creates a closure and hooks it into the 'default_site_option_*' filter.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string $option
 | 
			
		||||
	 * @param mixed  $value
 | 
			
		||||
	 */
 | 
			
		||||
	public function setDefault( $option, $value )
 | 
			
		||||
	{
 | 
			
		||||
		add_filter( 'default_site_option_' . $option, function () use ( $value ) {
 | 
			
		||||
			return $value;
 | 
			
		||||
		} );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function offsetExists( $option )
 | 
			
		||||
	{
 | 
			
		||||
		// Unfortunately, we can’t really tell whether it exists…
 | 
			
		||||
		$value = get_site_option( $option );
 | 
			
		||||
		return false !== $value && null !== $value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetGet( $option )
 | 
			
		||||
	{
 | 
			
		||||
		return get_site_option( $option );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetSet( $option, $value )
 | 
			
		||||
	{
 | 
			
		||||
		update_site_option( $option, $value );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function offsetUnset( $option )
 | 
			
		||||
	{
 | 
			
		||||
		delete_site_option( $option );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
abstract class Theme extends Plugin
 | 
			
		||||
{
 | 
			
		||||
	public function __construct()
 | 
			
		||||
	{
 | 
			
		||||
		$GLOBALS['content_width'] = $this->getContentWidth();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return integer
 | 
			
		||||
	 */
 | 
			
		||||
	abstract public function getContentWidth();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Try to load a Shy WordPress class.
 | 
			
		||||
 * 
 | 
			
		||||
 * @param string $name
 | 
			
		||||
 * @return boolean
 | 
			
		||||
 */
 | 
			
		||||
function shy_wordpress_autoloader( $name )
 | 
			
		||||
{
 | 
			
		||||
	if ( substr( $name, 0, 14 ) !== 'Shy\\WordPress\\' ) {
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$name = __DIR__ . '/' . str_replace( '\\', DIRECTORY_SEPARATOR, $name ) . '.php';
 | 
			
		||||
 | 
			
		||||
	return is_file( $name ) && include( $name );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
spl_autoload_register( 'shy_wordpress_autoloader' );
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress\Tests;
 | 
			
		||||
 | 
			
		||||
use Shy\WordPress\HookableTrait;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check that HookableTrait actually works.
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Philipp Cordes <pc@irgendware.net>
 | 
			
		||||
 */
 | 
			
		||||
class HookableTraitTest extends \WP_UnitTestCase
 | 
			
		||||
{
 | 
			
		||||
	use HookableTrait;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function actionMethod()
 | 
			
		||||
	{
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function testWorksAsAction()
 | 
			
		||||
	{
 | 
			
		||||
		$this->addHookMethod( 'shywp_test_action', 'actionMethod' );
 | 
			
		||||
		$this->assertTrue( has_action( 'shywp_test_action' ), 'Registering an action via addHookMethod() worked.' );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public function filterMethod( $value )
 | 
			
		||||
	{
 | 
			
		||||
		return $value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function testWorksAsFilter()
 | 
			
		||||
	{
 | 
			
		||||
		$this->addHookMethod( 'shywp_test_filter', 'filterMethod' );
 | 
			
		||||
		$this->assertTrue( has_filter( 'shywp_test_filter' ), 'Registering a filter via addHookMethod() worked.' );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,180 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Shy\WordPress\Tests;
 | 
			
		||||
 | 
			
		||||
use Shy\WordPress\SettingsPage;
 | 
			
		||||
use PHPUnit_Framework_MockObject_MockObject as MockObject;
 | 
			
		||||
use PHPUnit_Framework_MockObject_Builder_InvocationMocker as BuilderInvocationMocker;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SettingsPageTest extends \WP_UnitTestCase
 | 
			
		||||
{
 | 
			
		||||
	/**
 | 
			
		||||
	 * Mock a SettingsPage.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param string|null $slug
 | 
			
		||||
	 * @param string      $capability
 | 
			
		||||
	 * @return SettingsPage|MockObject {
 | 
			
		||||
	 *    @method BuilderInvocationMocker method(string)
 | 
			
		||||
	 * }
 | 
			
		||||
	 */
 | 
			
		||||
	protected function mockSettingsPage( $slug = null, $capability = 'manage_options' )
 | 
			
		||||
	{
 | 
			
		||||
		$builder = $this->getMockBuilder( 'Shy\WordPress\SettingsPage' )
 | 
			
		||||
			->enableProxyingToOriginalMethods();
 | 
			
		||||
 | 
			
		||||
		if ( null === $slug ) {
 | 
			
		||||
			$builder->disableOriginalConstructor();
 | 
			
		||||
		} else {
 | 
			
		||||
			$builder->setConstructorArgs( array( $slug, $capability ) );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return $builder->getMock();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Test reading defaults from the settings page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @covers SettingsPage::__construct()
 | 
			
		||||
	 * @covers SettingsPage::getDefaults()
 | 
			
		||||
	 * @covers SettingsPage::offsetExists()
 | 
			
		||||
	 * @covers SettingsPage::offsetGet()
 | 
			
		||||
	 * @expectedException OutOfBoundsException
 | 
			
		||||
	 */
 | 
			
		||||
	public function testReading()
 | 
			
		||||
	{
 | 
			
		||||
		$slug     = 'shywp_settingspage_test_slug_reading';
 | 
			
		||||
		$defaults = array( 'foo' => 'bar' );
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug );
 | 
			
		||||
		$page->method( 'getDefaults' )->willReturn( $defaults );
 | 
			
		||||
 | 
			
		||||
		$this->assertEquals( $defaults, get_option( $slug ) );
 | 
			
		||||
 | 
			
		||||
		$this->assertArrayHasKey( 'foo', $page );
 | 
			
		||||
		$this->assertEquals( $defaults['foo'], $page['foo'] );
 | 
			
		||||
 | 
			
		||||
		$this->assertArrayNotHasKey( 'baz', $page );
 | 
			
		||||
		$page['baz'];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Test writing to the settings page.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @covers SettingsPage::offsetSet()
 | 
			
		||||
	 * @expectedException OutOfBoundsException
 | 
			
		||||
	 */
 | 
			
		||||
	public function testWriting()
 | 
			
		||||
	{
 | 
			
		||||
		$slug     = 'shywp_settingspage_test_slug_writing';
 | 
			
		||||
		$defaults = array( 'foo' => 'bar' );
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug );
 | 
			
		||||
		$page->method( 'getDefaults' )->willReturn( $defaults );
 | 
			
		||||
 | 
			
		||||
		$page['foo'] = 'foo';
 | 
			
		||||
		$this->assertEquals( 'foo', $page['foo'] );
 | 
			
		||||
		$page['baz'] = '123';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Fail to remove a setting.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @covers SettingPage::offsetUnset()
 | 
			
		||||
	 * @expectedException BadMethodCallException
 | 
			
		||||
	 */
 | 
			
		||||
	public function testRemoving()
 | 
			
		||||
	{
 | 
			
		||||
		$slug = 'shywp_settingspage_test_slug_removing';
 | 
			
		||||
		$defaults = array();
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug );
 | 
			
		||||
		$page->method( 'getDefaults' )->willReturn( $defaults );
 | 
			
		||||
 | 
			
		||||
		unset( $page['baz'] );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Test whether the settings page can be showed.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @covers SettingsPage::__construct()
 | 
			
		||||
	 * @covers SettingsPage::getParentSlug()
 | 
			
		||||
	 * @covers SettingsPage::getPageTitle()
 | 
			
		||||
	 * @covers SettingsPage::getMenuTitle()
 | 
			
		||||
	 */
 | 
			
		||||
	public function testRegisterPage()
 | 
			
		||||
	{
 | 
			
		||||
		$this->expectOutputRegex( '/<page&title>/' );
 | 
			
		||||
 | 
			
		||||
		$slug = 'shywp_settingspage_test_slug_registerpage';
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug );
 | 
			
		||||
		$page->method( 'getParentSlug' )->willReturn( 'index.php' );
 | 
			
		||||
		$page->method( 'getPageTitle' )->willReturn( '<page&title>' );
 | 
			
		||||
		$page->method( 'getMenuTitle' )->willReturn( '<menu&title>' );
 | 
			
		||||
 | 
			
		||||
		$page->expects( $this->once() )->method( 'registerPage' )->with();
 | 
			
		||||
		$page->expects( $this->once() )->method( 'registerSettings' )->with();
 | 
			
		||||
 | 
			
		||||
		// FIXME: Simulate display of backend.
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @covers SettingsPage::sanitizeOptions()
 | 
			
		||||
	 */
 | 
			
		||||
	public function testSanitize()
 | 
			
		||||
	{
 | 
			
		||||
		$slug = 'shywp_settingspage_test_slug_sanitize';
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug );
 | 
			
		||||
		$page->method( 'sanitizeOptions' )->will( $this->returnArgument( 0 ) );
 | 
			
		||||
		$page->expects( $this->atLeastOnce() )->method( 'sanitizeOptions' );
 | 
			
		||||
 | 
			
		||||
		$this->markTestIncomplete();
 | 
			
		||||
		// FIXME: Simulate form submission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function testRenderTextField()
 | 
			
		||||
	{
 | 
			
		||||
		$this->expectOutputRegex( '/^<input type="text"/' );
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage();
 | 
			
		||||
		$page->renderTextField( array(
 | 
			
		||||
			'label_for' => 'foo',
 | 
			
		||||
			'name'      => 'bar',
 | 
			
		||||
		) );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function testRenderCheckboxField()
 | 
			
		||||
	{
 | 
			
		||||
		$this->expectOutputRegex( '/^<label><input type="checkbox"/' );
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage();
 | 
			
		||||
		$page->renderCheckboxField( array(
 | 
			
		||||
			'label_for' => 'foo',
 | 
			
		||||
			'name'      => 'bar',
 | 
			
		||||
			'caption'   => 'baz',
 | 
			
		||||
		) );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function testRenderPage()
 | 
			
		||||
	{
 | 
			
		||||
		$this->markTestIncomplete();
 | 
			
		||||
		$this->expectOutputRegex( '/<form action="options.php" method="post">.*<3&>.*cryptic_teaser.*</form>/' );
 | 
			
		||||
 | 
			
		||||
		$slug   = 'shywp_settingspage_test_slug_renderpage';
 | 
			
		||||
 | 
			
		||||
		$page = $this->mockSettingsPage( $slug, 'read' );
 | 
			
		||||
		$page->method( 'getPageTitle' )->willReturn( '<3&>' );
 | 
			
		||||
		$page->method( 'renderSectionTeaser' )->will( $this->returnCallback( function () use ( $teaser ) {
 | 
			
		||||
			echo 'cryptic_teaser';
 | 
			
		||||
		} ) );
 | 
			
		||||
 | 
			
		||||
		// FIXME: Simulate view of the settings page
 | 
			
		||||
		$page->renderPage();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Try to load a Shy WordPress test class.
 | 
			
		||||
 * 
 | 
			
		||||
 * @param string $name
 | 
			
		||||
 * @return boolean
 | 
			
		||||
 */
 | 
			
		||||
function shy_wordpress_tests_autoloader( $name )
 | 
			
		||||
{
 | 
			
		||||
	if ( substr( $name, 0, 20 ) !== 'Shy\\WordPress\\Tests\\' ) {
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$name = __DIR__ . '/' . str_replace( '\\', DIRECTORY_SEPARATOR, $name ) . '.php';
 | 
			
		||||
 | 
			
		||||
	return is_file( $name ) && include( $name );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
spl_autoload_register( 'shy_wordpress_tests_autoloader' );
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * PHPUnit bootstrap file
 | 
			
		||||
 * 
 | 
			
		||||
 * Variant of the one from github.com/tierra/wordpress-plugins-tests
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
require_once '../src/autoloader.php';
 | 
			
		||||
require_once 'autoloader.php';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
require_once ( getenv( 'WP_DEVELOP_DIR' ) ?: '../../../..' )
 | 
			
		||||
	. '/tests/phpunit/includes/bootstrap.php';
 | 
			
		||||
		Reference in New Issue
	
	Block a user