AstroFields/Core

View on GitHub
src/Mediators/Entity.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

namespace WCM\AstroFields\Core\Mediators;

use WCM\AstroFields\Core\Commands\CommandInterface;
use WCM\AstroFields\Core\Commands\ContextAwareInterface;
use WCM\AstroFields\Core\Helpers\ParserInterface;

/**
 * Class Entity
 * @package WCM\AstroFields\Core\Mediators
 */
class Entity extends \SplObjectStorage implements EntityInterface
{
    /** @type string */
    private $key;

    /** @type Array */
    private $types = array();

    /** @type Array */
    private $proxy = array();

    /** @var ParserInterface | string */
    private $parser = '\\WCM\\AstroFields\\Core\\Helpers\\ContextParser';

    public function __construct(
        $key = null,
        Array $types = array(),
        ParserInterface $parser = null
        )
    {
        $this->key   = $key;
        $this->types = $types;

        $this->parser = (
            is_null( $parser )
            AND is_string( $this->parser )
        )
            ? new $this->parser
            : new $parser;
    }

    public function getKey()
    {
        return $this->key;
    }

    /**
     * Attach {proxy} placeholders, which are usable in the `context`
     * Similar to {key} and {type}
     * @param Array | mixed $proxy
     * @return mixed|void
     */
    public function setProxy( $proxy )
    {
        ! is_array( $proxy ) and $proxy = array( $proxy );

        $this->proxy[] = $proxy;
    }

    /**
     * A quick dev helper that allows echo-ing the current Entity "ID"
     * @codeCoverageIgnore
     * @return string
     */
    public function __toString()
    {
        return sprintf( '%s@%s', __CLASS__, spl_object_hash( $this ) );
    }

    /**
     * Attach a Command to an Entity
     * This method also notifies the attached Command and
     * attaches it to its `context` (filters/actions)
     * Parses the Context with the Parser specific to this Entity.
     * @throws \InvalidArgumentException
     * @param CommandInterface $command
     * @param array            $data
     * @return $this|void
     */
    public function attach( $command, $data = array() )
    {
        if ( ! $command instanceof CommandInterface )
            throw new \InvalidArgumentException( 'Commands must implement the CommandInterface' );

        if (
            ! is_null( $data )
            and ! is_array( $data )
        )
            throw new \InvalidArgumentException( 'Command data must be an Array' );

        $data = $this->setupCommandData( $data );

        parent::attach( $command, $data );

        // Parse and attach context, notify Command and mark as notified
        if (
            $this->isContextAware( $command )
            and ! $data['notified']
            and is_object( $this->parser )
            )
        {
            /** @var ContextAwareInterface $command */
            $data = array_merge(
                $data,
                $this->parseContext( $command, $data )
            );
            $this->notify( $command, $data );
        }

        return $this;
    }

    /**
     * Merge Command data with defaults and preserves defaults silently
     * @param array $data
     * @return array
     */
    public function setupCommandData( Array $data )
    {
        return array(
            'key'      => $this->key,
            'types'    => $this->types,
            'notified' => false,
        ) + $data;
    }

    /**
     * Attach/Inject a Command bundle
     * This method allows merging the Commands of one Entity
     * into the current Entity. Already attached Commands do not get
     * overwritten/are skipped. The Callbacks of the old Entity' Commands
     * get removed from their respective filters and actions as keys and
     * types are attached to the Entity and not the Command.
     * @throws \InvalidArgumentException
     * @param \SplObjectStorage $commands
     */
    public function addAll( $commands )
    {
        if ( ! $commands instanceof \SplObjectStorage )
            throw new \InvalidArgumentException( 'Commands must implement SplObjectStorage' );

        /** @var \SplObjectStorage $commands */
        $commands->rewind();

        foreach ( $commands as $cmd )
        {
            if ( ! $commands->current() instanceof CommandInterface )
                throw new \InvalidArgumentException( 'Command must implement the CommandInterface' );

            /** @var CommandInterface $command */
            $command = $commands->current();
            if ( ! $this->contains( $command ) )
            {
                // Attach Commands to new filters
                $this->attach(
                    $command,
                    $commands->getInfo()
                );
                // Detach Command from old filters/actions
                $commands->detach( $command );
            }
        }
    }

    /**
     * Detach a Command
     * Also removes its callbacks on filters or actions.
     * @throws \InvalidArgumentException
     * @param CommandInterface $command
     * @return $this|void
     */
    public function detach( $command )
    {
        if ( ! $command instanceof CommandInterface )
            throw new \InvalidArgumentException( 'Commands must implement the CommandInterface' );

        // Remove callbacks from filter/action
        if ( $this->isContextAware( $command ) )
        {
            $data = $this->offsetGet( $command );

            foreach ( $data['context'] as $callback => $context )
                remove_filter( $context, $callback, 10  );
        }

        parent::detach( $command );

        return $this;
    }

    /**
     * Test if a class is aware of its `context`
     * @param CommandInterface $command
     * @return bool
     */
    public function isContextAware( CommandInterface $command )
    {
        return $command instanceof ContextAwareInterface
            and $this->getKey();
    }

    /**
     * Build the context (hooks/filters) array
     * When a context is provided when attaching a Command,
     * you can use `{key}`, `{type}` and `{proxy}` as placeholders
     * to be used in the Parser.
     * @param ContextAwareInterface $command
     * @param array            $data
     * @return array
     */
    public function parseContext( ContextAwareInterface $command, Array $data = array() )
    {
        $placeholder = array(
            '{key}'   => array( $this->key ),
            '{type}'  => $this->types,
            '{proxy}' => $this->proxy,
        );
        $this->parser->setup(
            $placeholder,
            $command->getContext()
        );

        return array_merge( $data, array(
            'context' => $this->parser->getResult(),
        ) );
    }

    /**
     * Delay the execution of a Command until the appearance of an action or filter
     * Important: The Entity is attached as clone to avoid altering the original
     * from inside a Command as this might affect other Commands.
     * It also allows the method to be called multiple times.
     * $subject = $this Alias:
     * PHP 5.3 fix, as Closures don't know where to point `$this` prior to 5.4
     * @link https://wiki.php.net/rfc/closures/object-extension
     * @codeCoverageIgnore
     * @param ContextAwareInterface $command
     * @param array             $data
     */
    public function notify( ContextAwareInterface $command, Array $data = array() )
    {
        /** @var Entity | \SplObjectStorage $subject */
        $subject = clone $this;

        $callbacks = array();
        foreach ( $data['context'] as $context )
        {
            /** @codeCoverageIgnore */
            $callback = function() use ( $command, $subject, $data )
            {
                $args = func_get_args();
                array_unshift( $args, $subject, $data );

                $subject->addInfo( array( 'frozen' => true, ), $command );

                /** @noinspection PhpVoidFunctionResultUsedInspection */
                return call_user_func_array(
                    array( $command, 'update' ),
                    $args
                );
            };

            // Set the hash of the {closure} as new index for the context
            $callbacks[] = _wp_filter_build_unique_id( $context, $callback, 10 );

            add_filter( $context, $callback, 10, PHP_INT_MAX -1 );
        }

        // Attach callback hashes to context array and set as notified
        $data['context'] = array_combine( $callbacks, $data['context'] );
        $data['notified'] = true;

        $this->addInfo( $data, $command );
    }

    /**
     * Add info to the info array of a Command
     * @param array            $data
     * @param ContextAwareInterface $command
     */
    public function addInfo( Array $data, ContextAwareInterface $command = null )
    {
        if ( ! is_null( $command ) )
        {
            $this->rewind();
            while ( $this->valid() )
            {
                if ( $this->current() === $command )
                    break;
                $this->next();
            }
        }

        $info = parent::getInfo() ?: array();
        parent::setInfo( array_merge( $info, $data ) );
    }
}