<?php

namespace Laravel\Spark\Http\Controllers\Auth;

use App\User;
use Validator;
use Exception;
use Carbon\Carbon;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use Stripe\Coupon as StripeCoupon;
use Illuminate\Support\Facades\DB;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Laravel\Spark\Subscriptions\Plan;
use Laravel\Spark\Events\User\Registered;
use Laravel\Spark\Events\User\Subscribed;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Laravel\Spark\Contracts\Repositories\UserRepository;
use Laravel\Spark\Contracts\Repositories\TeamRepository;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Laravel\Spark\Contracts\Auth\Subscriber as SubscriberContract;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins, ValidatesRequests;

    /**
     * The user repository instance.
     *
     * @var \Laravel\Spark\Contracts\Repositories\UserRepository
     */
    protected $users;

    /**
     * The team repository instance.
     *
     * @var \Laravel\Spark\Contracts\Repositories\TeamRepository
     */
    protected $teams;

    /**
     * The URI for the login route.
     *
     * @var string
     */
    protected $loginPath = '/login';

    /**
     * Create a new authentication controller instance.
     *
     * @param  \Laravel\Spark\Contracts\Repositories\UserRepository  $users
     * @param  \Laravel\Spark\Contracts\Repositories\TeamRepository  $teams
     * @return void
     */
    public function __construct(UserRepository $users, TeamRepository $teams)
    {
        $this->users = $users;
        $this->teams = $teams;
        $this->plans = Spark::plans();

        $this->middleware('guest', ['except' => 'getLogout']);
    }

    /**
     * Show the application login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function getLogin()
    {
        return view('spark::auth.authenticate');
    }

    /**
     * Send the post-authentication response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @return \Illuminate\Http\Response
     */
    protected function authenticated(Request $request, Authenticatable $user)
    {
        if (Spark::supportsTwoFactorAuth() && Spark::twoFactorProvider()->isEnabled($user)) {
            return $this->logoutAndRedirectToTokenScreen($request, $user);
        }

        return redirect()->intended($this->redirectPath());
    }

    /**
     * Generate a redirect response to the two-factor token screen.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $users
     * @return \Illuminate\Http\Response
     */
    protected function logoutAndRedirectToTokenScreen(Request $request, Authenticatable $user)
    {
        Auth::logout();

        $request->session()->put('spark:auth:id', $user->id);

        return redirect('login/token');
    }

    /**
     * Show the two-factor authentication token verification form.
     *
     * @return \Illuminate\Http\Response
     */
    public function getToken()
    {
        return session('spark:auth:id') ? view('spark::auth.token') : redirect('login');
    }

    /**
     * Verify the two-factor authentication token.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function postToken(Request $request)
    {
        $this->validate($request, ['token' => 'required']);

        if (! session('spark:auth:id')) {
            return redirect('login');
        }

        $model = config('auth.model');

        $user = (new $model)->findOrFail(
            $request->session()->pull('spark:auth:id')
        );

        if (Spark::twoFactorProvider()->tokenIsValid($user, $request->token)) {
            Auth::login($user);

            return redirect()->intended($this->redirectPath());
        } else {
            return back();
        }
    }

    /**
     * Show the application registration form.
     *
     * @param  \Illuminate\Http\Request
     * @return \Illuminate\Http\Response
     */
    public function getRegister(Request $request)
    {
        if (Spark::forcingPromotion() && ! $request->query('coupon')) {
            if (count($request->query()) > 0) {
                return redirect($request->fullUrl().'&coupon='.Spark::forcedPromotion());
            } else {
                return redirect($request->fullUrl().'?coupon='.Spark::forcedPromotion());
            }
        }

        if (count($this->plans->paid()) > 0) {
            return view('spark::auth.registration.subscription');
        } else {
            return view('spark::auth.registration.simple');
        }
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function postRegister(Request $request)
    {
        $withSubscription = count($this->plans->paid()) > 0 &&
                            $this->plans->find($request->plan) &&
                            $this->plans->find($request->plan)->price > 0;

        return $this->register($request, $withSubscription);
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $withSubscription
     * @return \Illuminate\Http\Response
     */
    protected function register(Request $request, $withSubscription = false)
    {
        $this->validateRegistration($request, $withSubscription);

        $user = $this->users->createUserFromRegistrationRequest(
            $request, $withSubscription
        );

        if ($request->team_name) {
            $team = $this->teams->create($user, ['name' => $request->team_name]);
        }

        if ($request->invitation) {
            $this->teams->attachUserToTeamByInvitation($request->invitation, $user);
        }

        event(new Registered($user));

        if ($withSubscription) {
            event(new Subscribed($user));
        }

        Auth::login($user);

        return response()->json(['path' => $this->redirectPath()]);
    }

    /**
     * Validate the new registration.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $withSubscription
     * @return void
     */
    protected function validateRegistration(Request $request, $withSubscription = false)
    {
        if (Spark::$validateProfileUpdatesWith) {
            $this->callCustomValidator(
                Spark::$validateProfileUpdatesWith, $request, [$withSubscription]
            );
        } else {
            $this->validateDefaultRegistration($request, $withSubscription);
        }
    }

    /**
     * Validate a new registration using the default rules.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $withSubscription
     * @return void
     */
    protected function validateDefaultRegistration(Request $request, $withSubscription)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|confirmed|min:6',
            'terms' => 'required|accepted',
        ]);

        if ($withSubscription) {
            $validator->mergeRules('stripe_token', 'required');

            if ($request->coupon) {
                $validator->after(function ($validator) use ($request) {
                    $this->validateCoupon($validator, $request);
                });
            }
        }

        if ($validator->fails()) {
            $this->throwValidationException($request, $validator);
        }
    }

    /**
     * Validate that the provided coupon actually exists on Stripe.
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    protected function validateCoupon($validator, Request $request)
    {
        try {
            $coupon = StripeCoupon::retrieve(
                $request->coupon, ['api_key' => config('services.stripe.secret')]
            );

            if ($coupon && $coupon->valid) {
                return;
            }
        } catch (Exception $e) {
            //
        }

        $validator->errors()->add('coupon', 'The provided coupon code is invalid.');
    }

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function getLogout(Request $request)
    {
        $request->session()->flush();

        Auth::logout();

        return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
    }

    /**
     * Get the post register / login redirect path.
     *
     * @return string
     */
    public function redirectPath()
    {
        return Spark::$afterAuthRedirectTo;
    }
}


<?php

namespace Grapesc\GrapeFluid\Application;

use Grapesc\GrapeFluid\AssetRepository;
use Grapesc\GrapeFluid\AssetsControl\AssetsControl;
use Grapesc\GrapeFluid\BaseParametersRepository;
use Grapesc\GrapeFluid\CoreModule\Service\BreadCrumbService;
use Grapesc\GrapeFluid\CoreModule\Model\SettingModel;
use Grapesc\GrapeFluid\FluidTranslator;
use Grapesc\GrapeFluid\ImageStorage;
use Grapesc\GrapeFluid\LinkCollector\LinkCollector;
use Grapesc\GrapeFluid\MagicControl\Creator;
use Grapesc\GrapeFluid\MagicControl\MagicControl;
use Grapesc\GrapeFluid\ModuleRepository;
use Grapesc\GrapeFluid\ScriptCollector;
use Grapesc\GrapeFluid\Security\NamespacesRepository;
use Nette\Caching\Storages\FileStorage;
use Nette\Forms\Form;

abstract class BasePresenter extends \Nette\Application\UI\Presenter
{

	/** @var Creator @inject */
	public $magicControlCreator;

	/** @var AssetRepository @inject */
	public $assets;

	/** @var BaseParametersRepository @inject */
	public $appParams;

	/** @var FluidTranslator @inject */
	public $translator;

	/** @var FileStorage @inject */
	public $storage;

	/** @var SettingModel @inject */
	public $setting;

	/** @var LinkCollector @inject */
	public $collector;

	/** @var ScriptCollector @inject */
	public $scriptCollector;

	/** @var ImageStorage @inject */
	public $imageStorage;

	/** @var BreadCrumbService @inject */
	public $breadCrumbService;

	/** @var ActionTemplateOverloading @inject */
	public $actionTemplateOverloading;

	/** @var ModuleRepository @inject */
	public $moduleRepository;

	/** @var NamespacesRepository @inject */
	public $namespacesRepository;

	/** @var AssetsControl @inject */
	public $assetsControl;

	/**	@var bool @persistent */
	public $_noLayout = false;

	/** @var string */
	protected $defaultNamespace = 'frontend';

	public function formatLayoutTemplateFiles()
	{
		$list = parent::formatLayoutTemplateFiles();
		//todo zkontrolovat
//		$list[] = $this->appParams->getParam('libDir') . $this->layout . '.latte';
		return $list;
	}

	protected function startup()
	{
		parent::startup();
		$this->setLayout(__DIR__ . DIRECTORY_SEPARATOR . 'template' . DIRECTORY_SEPARATOR . '@layout.latte');
		$this->setSubLayout($this->context->getParameters()['project']['layout']);
		$this->setFlashMessagesFile($this->context->getParameters()['project']['flashes']);
		$this->setMeta([
			//cremeta foreachnout a nastavit
			"title"       => $this->setting->getVal("core.meta.title"),
			"keywords"    => $this->setting->getVal("core.meta.keywords"),
			"description" => $this->setting->getVal("core.meta.description"),
			"author"      => $this->setting->getVal("core.meta.author")
		]);

		$this->actionTemplateOverloading->override($this);
		$this->namespacesRepository->setCurrentNamespace($this->defaultNamespace);

		$this->breadCrumbService->addLink($this->translator->translate('Úvod'), rtrim($this->getHttpRequest()->getUrl()->getBaseUrl(), '/'));
	}

	protected function beforeRender()
	{
		parent::beforeRender();
		$this->template->_noLayout = $this->_noLayout;
	}

	protected function createComponent($name)
	{
		if (substr($name, 0, 3) == 'mc_') {
			$exploded = explode("_", $name);

			unset($exploded[sizeof($exploded) - 1]);
			unset($exploded[0]);
			$magicControl = implode("_", $exploded);

			if ($this->magicControlCreator->magicControlExist($magicControl)) {
				return new MagicControl(substr($name, strlen($magicControl) + 4), $this->magicControlCreator, $magicControl);
			}
		}

		$component = parent::createComponent($name);
		if ($component instanceof Form) {
			$component->setTranslator($this->translator);
		}

		return $component;
	}

	protected function createTemplate($class = NULL)
	{
		/** @var \Nette\Bridges\ApplicationLatte\Template $template */
		$template = parent::createTemplate();
		$template->setTranslator($this->translator);
		return $template;
	}

	public function createComponentAssets()
	{
		return $this->assetsControl;
	}

	/**
	 * Vrátí jméno aktuálního modulu podle aktuálního namespace presenteru
	 *
	 * @return mixed
	 */
	public function getModuleName()
	{
		return str_replace("Module", "", explode("\\", get_class($this))[2]);
	}

	/**
	 * Možnost nastavit meta tagy webu ve formátu:
	 * [ $argumentName => 'value' ]
	 *
	 * @param array $meta
	 * @param string $argumentName
	 */
	public function setMeta(array $meta, $argumentName = 'name')
	{
		/** @var \Nette\Bridges\ApplicationLatte\Template $template */
		$template = $this->template;

		if (!isset($template->meta)) {
			$template->meta = [];
		}

		if (!key_exists($argumentName, $template->meta)) {
			$template->meta[$argumentName] = [];
		}

		foreach ($meta as $var => $val) {
			$template->meta[$argumentName][$var] = $val;
		}
	}

	/**
	 * @param string $path
	 */
	protected function setSubLayout($path)
	{
		if (!$this->_noLayout) {
			$this->template->sublayout = $path;
		}
	}

	/**
	 * @param string $path
	 * @return void
	 */
	protected function setFlashMessagesFile($path)
	{
		$this->template->flashMessagesFile = $path;
	}

	/**
	 * Flash zprava s podporou translatoru
	 *
	 * @param $message
	 * @param string $type
	 * @return \stdClass
	 */
	public function flashMessage($message, $type = 'info')
	{
		return parent::flashMessage($this->translator->translate($message), $type);
	}

	/**
	 * @param string $moduleName
	 * @return bool
	 */
	public function ifModuleExists($moduleName)
	{
		return $this->moduleRepository->moduleExist($moduleName);
	}

}


<?php

namespace danog\PHP;

/**
 * PHPStruct
 * PHP implementation of Python's struct module.
 * This library was created to help me develop a [client for the mtproto protocol](https://github.com/danog/MadelineProto).
 * The functions and the formats are exactly the ones used in python's struct (https://docs.python.org/3/library/struct.html)
 * For now custom byte size may not work properly on certain machines for the i, I, f and d formats.
 *
 * @author		Daniil Gentili <daniil@daniil.it>
 * @license		MIT license
 */
// Struct class (for dynamic access)
class StructClass
{
    public $struct = null; // Will store an instance of the StructTools class
    public $format = null; // Will contain the format
    public $size = null; // Will contain the size

    /**
     * Constructor.
     *
     * Stores instance of the StructTools class and optional format/size
     */
    public function __construct($format = null)
    {
        $this->struct = new \danog\PHP\StructTools();
        if ($format !== null) {
            $this->format = $format;
            $this->size = $this->struct->calcsize($format);
        }
    }

    /**
     * pack.
     *
     * Packs data into bytes
     *
     * @param	...$data	Parameters to encode (may contain format string)
     *
     * @return Encoded data
     */
    public function pack(...$data)
    {
        if ($this->format === null) {
            $format = array_shift($data);
        } else {
            $format = $this->format;
        }

        return $this->struct->pack($format, ...$data);
    }

    /**
     * unpack.
     *
     * Unpacks data into an array
     *
     * @param	$format_maybe_data	Format string (may be data if class is istantiated with format string)
     * @param	$data	            Data to decode
     *
     * @return Decoded data
     */
    public function unpack($format_maybe_data, $data = null)
    {
        if ($this->format === null) {
            $format = $format_maybe_data;
        } else {
            $format = $this->format;
            $data = $format_maybe_data;
        }

        return $this->struct->unpack($format, $data);
    }

    /**
     * calcsize.
     *
     * Return the size of the struct (and hence of the string) corresponding to the given format.
     *
     *
     * @param	$format	Format string
     *
     * @return int with size of the struct.
     */
    public function calcsize($format = null)
    {
        return ($this->format !== null && $this->size !== null) ? $this->size : $this->struct->calcsize($format);
    }
}


<?php

namespace danog\PHP;

/**
 * PHPStruct
 * PHP implementation of Python's struct module.
 * This library was created to help me develop a [client for the mtproto protocol](https://github.com/danog/MadelineProto).
 * The functions and the formats are exactly the ones used in python's struct (https://docs.python.org/3/library/struct.html)
 * For now custom byte size may not work properly on certain machines for the i, I, f and d formats.
 *
 * @author		Daniil Gentili <daniil@daniil.it>
 * @license		MIT license
 */
// Working class
class StructTools
{
    /**
     * Constructor.
     *
     * Sets modifiers and gets endianness
     */
    public function __construct($format = null)
    {
        $this->BIG_ENDIAN = (pack('L', 1) === pack('N', 1));
        $this->IS64BIT = (PHP_INT_SIZE === 8);
        $this->FORMATS = [
            // Integer formats
            'b' => 'c', // should be 1 (8 bit)
            'B' => 'C', // should be 1 (8 bit)
            'h' => 's', // should be 2 (16 bit)
            'H' => 'S', // should be 2 (16 bit)
            'i' => 'l', // should be 4 (32 bit)
            'I' => 'L', // should be 4 (32 bit)
            'l' => 'l', // should be 4 (32 bit)
            'L' => 'L', // should be 4 (32 bit)
            'q' => 'q', // should be 8 (64 bit)
            'Q' => 'Q', // should be 8 (64 bit)

            // Floating point numbers
            'f' => 'f', // should be 4 (32 bit)
            'd' => 'd', // should be 8 (64 bit)

            // String formats (note that these format chars require a single parameter, regardless of the format char count)
            's' => 'a',
            'p' => 'p', // “Pascal string”, meaning a short variable-length string stored in a fixed number of bytes, given by the count. The first byte stored is the length of the string, or 255, whichever is smaller. The bytes of the string follow. If the string passed in to pack() is too long (longer than the count minus 1), only the leading count-1 bytes of the string are stored. If the string is shorter than count-1, it is padded with null bytes so that exactly count bytes in all are used. Note that for unpack(), the 'p' format character consumes count bytes, but that the string returned can never contain more than 255 characters.

            // char formats
            'c' => 'a',

            // Boolean formats
            '?' => '?',

            // Null
            'x' => 'x',
        ];
        $this->NATIVE_FORMATS = array_replace($this->FORMATS, [
            // These formats need to be modified after/before encoding/decoding.
            'P' => $this->IS64BIT ? 'Q' : 'L', // integer or long integer, depending on the size needed to hold a pointer when it has been cast to an integer type. A NULL pointer will always be returned as the Python integer 0. When packing pointer-sized values, Python integer or long integer objects may be used. For example, the Alpha and Merced processors use 64-bit pointer values, meaning a Python long integer will be used to hold the pointer; other platforms use 32-bit pointers and will use a Python integer.
            'n' => $this->IS64BIT ? 'q' : 'l',
            'N' => $this->IS64BIT ? 'Q' : 'L',
            'l' => $this->IS64BIT ? 'q' : 'l',
            'L' => $this->IS64BIT ? 'Q' : 'L',
        ]);
        $this->SIZE = [
            // Integer formats
            'b' => 1,
            'B' => 1,
            'h' => 2,
            'H' => 2,
            'i' => 4,
            'I' => 4,
            'l' => 4,
            'L' => 4,
            'q' => 8,
            'Q' => 8,

            // Floating point numbers
            'f' => 4,
            'd' => 8,

            // String formats (note that these format chars require a single parameter, regardless of the format char count)
            's' => 1,
            'p' => 1,

            // char formats
            'c' => 1,

            // Boolean formats
            '?' => 1,

            // Null
            'x' => 1,
        ];
        // Native length table for the @ modifier
        $this->NATIVE_SIZE = [
            // Integer formats
            'b' => strlen(pack($this->NATIVE_FORMATS['b'], 11)),
            'B' => strlen(pack($this->NATIVE_FORMATS['B'], 11)),
            'h' => strlen(pack($this->NATIVE_FORMATS['h'], -700)),
            'H' => strlen(pack($this->NATIVE_FORMATS['H'], 700)),
            'i' => strlen(pack($this->NATIVE_FORMATS['i'], 1)),
            'I' => strlen(pack($this->NATIVE_FORMATS['I'], 1)),
            'l' => strlen(pack($this->NATIVE_FORMATS['l'], -700)),
            'L' => strlen(pack($this->NATIVE_FORMATS['L'], 700)),
            'q' => $this->IS64BIT ? strlen(pack($this->NATIVE_FORMATS['q'], 700)) : 8,
            'Q' => $this->IS64BIT ? strlen(pack($this->NATIVE_FORMATS['Q'], 700)) : 8,

            // Floating point formats
            'f' => strlen(pack($this->NATIVE_FORMATS['f'], 2.0)),
            'd' => strlen(pack($this->NATIVE_FORMATS['d'], 2.0)),

            // String formats (note that these format chars require a single parameter, regardless of the format char count)
            'p' => 1,
            's' => strlen(pack($this->NATIVE_FORMATS['s'], 'c')),

            // Char formats
            'c' => strlen(pack($this->NATIVE_FORMATS['c'], 'a')),

            // Boolean formats
            '?' => strlen(pack('c', false)),

            // Null
            'x' => strlen(pack($this->NATIVE_FORMATS['x'])),

            // Automatical length formats
            'P' => strlen(pack($this->NATIVE_FORMATS['P'], 2323)),
            'n' => strlen(pack($this->NATIVE_FORMATS['n'], 1)),
            'N' => strlen(pack($this->NATIVE_FORMATS['N'], 1)),
        ];
        $this->TYPE = [
            // Integer formats
            'b' => 'int',
            'B' => 'int',
            'h' => 'int',
            'H' => 'int',
            'i' => 'int',
            'I' => 'int',
            'l' => 'int',
            'L' => 'int',
            'q' => 'int',
            'Q' => 'int',

             // Floating point formats
            'f' => 'float',
            'd' => 'float',

            // String formats
            'p' => 'string',
            's' => 'string',

            // Char formats
            'c' => 'string',

            // Boolean formats
            '?' => 'bool',

            // Null
            'x' => 'unset',
        ];
        $this->NATIVE_TYPE = array_merge([
            // These formats need to be modified after/before encoding/decoding.
            'P' => $this->IS64BIT ? $this->TYPE['Q'] : $this->TYPE['L'], // integer or long integer, depending on the size needed to hold a pointer when it has been cast to an integer type. A NULL pointer will always be returned as the Python integer 0. When packing pointer-sized values, Python integer or long integer objects may be used. For example, the Alpha and Merced processors use 64-bit pointer values, meaning a Python long integer will be used to hold the pointer; other platforms use 32-bit pointers and will use a Python integer.
            'n' => $this->IS64BIT ? $this->TYPE['q'] : $this->TYPE['l'],
            'N' => $this->IS64BIT ? $this->TYPE['Q'] : $this->TYPE['L'],
        ], $this->TYPE);
        $this->ENDIANNESS_TABLE = [
            'h' => true,
            'H' => true,
            'i' => true,
            'I' => true,
            'l' => true,
            'L' => true,
            'q' => true,
            'Q' => true,

            'n' => true,
            'N' => true,
            'P' => true,
            'f' => $this->BIG_ENDIAN,
            'd' => $this->BIG_ENDIAN,

        ];
        $this->LITTLE_ENDIAN_TABLE = array_merge($this->ENDIANNESS_TABLE, array_fill_keys(['x', 'c', 'b', 'B', '?', 's', 'p'], false));
        $this->BIG_ENDIAN_TABLE = array_merge($this->ENDIANNESS_TABLE, array_fill_keys(['x', 'c', 'b', 'B', '?', 's', 'p'], true));
        $this->NATIVE_ENDIAN_TABLE = $this->BIG_ENDIAN ? $this->BIG_ENDIAN_TABLE : $this->LITTLE_ENDIAN_TABLE;

        $this->MODIFIERS = [
            '<' => [
                'BIG_ENDIAN' => false,
                'ENDIANNESS' => $this->LITTLE_ENDIAN_TABLE,
                'SIZE'       => $this->SIZE,
                'FORMATS'    => $this->FORMATS,
                'TYPE'       => $this->TYPE,
                'MODIFIER'   => '<',
            ],
            '>' => [
                'BIG_ENDIAN' => true,
                'ENDIANNESS' => $this->BIG_ENDIAN_TABLE,
                'SIZE'       => $this->SIZE,
                'FORMATS'    => $this->FORMATS,
                'TYPE'       => $this->TYPE,
                'MODIFIER'   => '>',
            ],
            '!' => [
                'BIG_ENDIAN' => true,
                'ENDIANNESS' => $this->BIG_ENDIAN_TABLE,
                'SIZE'       => $this->SIZE,
                'FORMATS'    => $this->FORMATS,
                'TYPE'       => $this->TYPE,
                'MODIFIER'   => '!',
            ],
            '=' => [
                'BIG_ENDIAN' => $this->BIG_ENDIAN,
                'ENDIANNESS' => $this->NATIVE_ENDIAN_TABLE,
                'SIZE'       => $this->SIZE,
                'FORMATS'    => $this->FORMATS,
                'TYPE'       => $this->TYPE,
                'MODIFIER'   => '=',
            ],
            '@' => [
                'BIG_ENDIAN' => $this->BIG_ENDIAN,
                'ENDIANNESS' => $this->NATIVE_ENDIAN_TABLE,
                'SIZE'       => $this->NATIVE_SIZE,
                'FORMATS'    => $this->NATIVE_FORMATS,
                'TYPE'       => $this->NATIVE_TYPE,
                'MODIFIER'   => '@',
            ],
        ];
    }

    /**
     * ExceptionErrorHandler.
     *
     * Error handler for pack and unpack
     */
    public function ExceptionErrorHandler($errno = 0, $errstr = null, $errfile = null, $errline = null)
    {
        // If error is suppressed with @, don't throw an exception
        if (error_reporting() === 0) {
            return true; // return true to continue through the others error handlers
        }

        throw new StructException($errstr.' on line '.$errline, $errno);
    }

    /**
     * pack.
     *
     * Packs data into bytes
     *
     * @param	$format		Format string
     * @param	...$data	Parameters to encode
     *
     * @return Encoded data
     */
    public function pack($format, ...$data)
    {
        $format = $this->padformat($format);
        $result = null; // Data to return
        $size = $this->calcsize($format);
        $packcommand = $this->parseformat($format, $this->array_each_strlen($data)); // Get pack parameters
        set_error_handler([$this, 'ExceptionErrorHandler']);
        foreach ($packcommand as $key => $command) {
            try {
                switch ($command['modifiers']['TYPE']) {
                    case 'int':
                        if (!is_int($data[$command['datakey']]) && !is_float($data[$command['datakey']])) {
                            $data[$command['datakey']] = (int) $data[$command['datakey']];
                        }
                        break;
                    case 'float':
                        if (!is_float($data[$command['datakey']])) {
                            $data[$command['datakey']] = (float) $data[$command['datakey']];
                        }
                        break;
                    case 'string':
                        if (!is_string($data[$command['datakey']])) {
                            $data[$command['datakey']] = (string) $data[$command['datakey']];
                        }
                        break;
                    case 'bool':
                        if (!is_bool($data[$command['datakey']])) {
                            $data[$command['datakey']] = (bool) $data[$command['datakey']];
                        }
                        break;
                    default:
                        break;
                }
                switch ($command['phpformat']) {
                    case 'x':
                        $curresult = pack($command['phpformat'].$command['count']); // Pack current char
                        break;
                    case 'p':
                        $curresult = pack('c', ($command['count'] - 1 > 255) ? 255 : $command['count'] - 1).pack('a'.($command['count'] - 1), $data[$command['datakey']]);
                        break;

                    case 'q':
                    case 'Q':
                    case 'l':
                    case 'L':
                    case 'i':
                    case 'I':
                    case 's':
                    case 'S':
                    case 'c':
                    case 'C':
                        $curresult = $this->num_pack($data[$command['datakey']], $command['modifiers']['SIZE'], ctype_upper($command['phpformat']));

                        break;

                    case '?':
                        $curresult = pack('c'.$command['count'], $data[$command['datakey']]); // Pack current char
                        break;
                    default:
                        $curresult = pack($command['phpformat'].$command['count'], $data[$command['datakey']]); // Pack current char
                        break;
                }
                if (strlen($curresult) != $command['modifiers']['SIZE'] * $command['count']) {
                    trigger_error('Size of packed data '.strlen($curresult)." isn't equal to expected size ".$command['modifiers']['SIZE'] * $command['count'].'.');
                }
            } catch (StructException $e) {
                throw new StructException('An error occurred while packing '.$data[$command['datakey']].' at format key '.$command['format'].' ('.$e->getMessage().').');
            }
            if ($command['modifiers']['FORMAT_ENDIANNESS'] != $command['modifiers']['BIG_ENDIAN']) {
                $curresult = strrev($curresult);
            } // Reverse if wrong endianness
            /*
            if (strlen($curresult) > $command['modifiers']['SIZE'] * $command['count']) {
                if ($command['modifiers']['BIG_ENDIAN']) {
                    $curresult = strrev($curresult);
                }
                $remains = array_slice(str_split($curresult), $command['modifiers']['SIZE'], strlen($curresult) - $command['modifiers']['SIZE']);
                foreach ($remains as $rem) {
                    if ($rem != '') {
                        throw new StructException('Error while trimming result at offset '.$key.' (format char '.$command['format']."): data to trim isn't empty.");
                    }
                }
                $curresult = implode('', substr($curresult, 0, $command['modifiers']['SIZE']));
                if ($command['modifiers']['BIG_ENDIAN']) {
                    $curresult = strrev($curresult);
                }
            }*/
            $result .= $curresult;
        }
        restore_error_handler();
        if (strlen($result) != $size) {
            throw new StructException('Length of generated data ('.strlen($result).') is different from length calculated using format string ('.$size.').');
        }

        return $result;
    }

    /**
     * unpack.
     *
     * Unpacks data into an array
     *
     * @param	$format	Format string
     * @param	$data	Data to decode
     *
     * @return Decoded data
     */
    public function unpack($format, $data)
    {
        $format = $this->padformat($format);
        $size = $this->calcsize($format);
        if (strlen($data) != $size) {
            throw new StructException('Length of given data ('.strlen($data).') is different from length calculated using format string ('.$size.').');
        }
        $dataarray = $this->data2array($format, $data);

        if ($this->array_total_strlen($dataarray) != $size) {
            throw new StructException('Length of array ('.$this->array_total_strlen($dataarray).') is different from length calculated using format string ('.$size.').');
        }
        $result = []; // Data to return
        $packcommand = $this->parseformat($format, $this->array_each_strlen($dataarray), true); // Get unpack parameters
        set_error_handler([$this, 'ExceptionErrorHandler']);
        $arraycount = 0;
        foreach ($packcommand as $key => $command) {
            if ($command['modifiers']['FORMAT_ENDIANNESS'] != $command['modifiers']['BIG_ENDIAN']) {
                $dataarray[$command['datakey']] = strrev($dataarray[$command['datakey']]);
            } // Reverse if wrong endianness
            try {
                switch ($command['phpformat']) {
                    case 'p':
                        $templength = unpack('c', $dataarray[$command['datakey']])[1];
                        $result[$arraycount] = implode('', unpack('a'.$templength, substr($dataarray[$command['datakey']], 1)));
                        break;
                    case '?':
                        if (implode('', unpack('c'.$command['count'], $dataarray[$command['datakey']])) == 0) {
                            $result[$arraycount] = false;
                        } else {
                            $result[$arraycount] = true;
                        }
                        break;

                    case 'q':
                    case 'Q':
                    case 'l':
                    case 'L':
                    case 'i':
                    case 'I':
                    case 's':
                    case 'S':
                    case 'c':
                    case 'C':
                        $result[$arraycount] = $this->num_unpack($dataarray[$command['datakey']], $command['modifiers']['SIZE'], ctype_upper($command['phpformat']));
                        break;

                    default:
                        $result[$arraycount] = implode('', unpack($command['phpformat'].$command['count'], $dataarray[$command['datakey']])); // Unpack current char
                        break;
                }
            } catch (StructException $e) {
                throw new StructException('An error occurred while unpacking data at offset '.$command['datakey'].' ('.$e->getMessage().').');
            }
            switch ($command['modifiers']['TYPE']) {
                case 'int':
                    if (!is_int($result[$arraycount]) && !is_float($result[$arraycount])) {
                        $result[$arraycount] = (int) $result[$arraycount];
                    }
/*                    if (is_float($result[$arraycount]) && $result[$arraycount] < PHP_INT_MAX) {
                        $result[$arraycount] = (int) $result[$arraycount];
                    }*/
                    break;
                case 'float':
                    if (!is_float($result[$arraycount])) {
                        $result[$arraycount] = (float) $result[$arraycount];
                    }

                    break;
                case 'unset':
                    unset($result[$arraycount]);
                    $arraycount--;
                    break;
                case 'string':
                    if (!is_string($result[$arraycount])) {
                        $result[$arraycount] = (string) $result[$arraycount];
                    }
                    break;
                case 'bool':
                    if (!is_bool($result[$arraycount])) {
                        $result[$arraycount] = (bool) $result[$arraycount];
                    }
                    break;
                default:
                    break;
            }
            $arraycount++;
        }
        restore_error_handler();

        return $result;
    }

    /**
     * calcsize.
     *
     * Return the size of the struct (and hence of the string) corresponding to the given format.
     *
     *
     * @return int with size of the struct.
     */
    public function calcsize($format)
    {
        $format = $this->padformat($format);
        $size = 0;
        $modifier = $this->MODIFIERS['@'];
        $count = null;
        if ($format == '') {
            return 0;
        }
        foreach (str_split($format) as $offset => $currentformatchar) {
            if (isset($this->MODIFIERS[$currentformatchar])) {
                $modifier = $this->MODIFIERS[$currentformatchar]; // Set the modifiers for the current format char
            } elseif (is_numeric($currentformatchar) && ((int) $currentformatchar > 0 || (int) $currentformatchar <= 9)) {
                $count .= $currentformatchar; // Set the count for the current format char
            } elseif (isset($modifier['SIZE'][$currentformatchar])) {
                if (!isset($count) || $count == null) {
                    $count = 1; // Set count to 1 if something's wrong.
                }
                $size += $count * $modifier['SIZE'][$currentformatchar];
                $count = null;
            } else {
                throw new StructException('Unkown format or modifier supplied ('.$currentformatchar.' at offset '.$offset.').');
            }
        }

        return $size;
    }

    /**
     * parseformat.
     *
     * Parses format string.
     *
     * @param	$arraycount Array containing the number of chars contained in each element of the array to pack
     *
     * @throws StructException if format string is too long or there aren't enough parameters or if an unkown format or modifier is supplied.
     *
     * @return array with format and modifiers for each object to encode/decode
     */
    public function parseformat($format, $arraycount, $unpack = false)
    {
        $datarraycount = 0; // Current element of the array to pack/unpack
        $formatcharcount = 0; // Current element to pack/unpack (sometimes there isn't a correspondant element in the array)
        $modifier = $this->MODIFIERS['@'];
        $result = []; // Array with the results
        $count = null;
        $loopcount = 0;
        $totallength = 0;
        foreach (str_split($format) as $offset => $currentformatchar) { // Current format char
            if (!isset($result[$formatcharcount]) || !is_array($result[$formatcharcount])) {
                $result[$formatcharcount] = []; // Create array for current element
            }
            if (isset($this->MODIFIERS[$currentformatchar])) { // If current format char is a modifier
                $modifier = $this->MODIFIERS[$currentformatchar]; // Set the modifiers for the current format char
            } elseif (is_numeric($currentformatchar) && ((int) $currentformatchar >= 0 || (int) $currentformatchar <= 9)) {
                $count .= (int) $currentformatchar; // Set the count for the current format char
            } elseif (isset($modifier['FORMATS'][$currentformatchar])) {
                if (!isset($count) || $count == null) {
                    $count = 1; // Set count to 1 by default.
                }
                $count = (int) $count;
                if ($currentformatchar == 's' || $currentformatchar == 'p') {
                    $loopcount = 1;
                } else {
                    $loopcount = $count;
                    $count = 1;
                }
                for ($x = 0; $x < $loopcount; $x++) {
                    $result[$formatcharcount]['format'] = $currentformatchar; // Set format
                    $result[$formatcharcount]['phpformat'] = $modifier['FORMATS'][$currentformatchar]; // Set format
                    $result[$formatcharcount]['count'] = $count;
                    $result[$formatcharcount]['modifiers'] = [
                        'BIG_ENDIAN'        => $modifier['BIG_ENDIAN'],
                        'FORMAT_ENDIANNESS' => $modifier['ENDIANNESS'][$currentformatchar],
                        'SIZE'              => $modifier['SIZE'][$currentformatchar],
                        'TYPE'              => $modifier['TYPE'][$currentformatchar],
                    ];
                    if ($unpack) {
                        if ($arraycount[$datarraycount] != $result[$formatcharcount]['count'] * $result[$formatcharcount]['modifiers']['SIZE']) {
                            throw new StructException('Length for format string '.$result[$formatcharcount]['format'].' at offset '.$offset.' ('.$result[$formatcharcount]['count'] * $result[$formatcharcount]['modifiers']['SIZE'].") isn't equal to the length of associated parameter (".$arraycount[$datarraycount].').');
                        }
                        $result[$formatcharcount]['datakey'] = $datarraycount;
                        $datarraycount++;
                    } else {
                        if ($currentformatchar != 'x') {
                            $result[$formatcharcount]['datakey'] = $datarraycount;
                            $datarraycount++;
                        }
                    }
                    if ($datarraycount > count($arraycount)) {
                        throw new StructException('Format string too long or not enough parameters at offset '.$offset.' ('.$currentformatchar.').');
                    }
                    $formatcharcount++; // Increase element count
                }
                $count = null;
            } else {
                throw new StructException('Unkown format or modifier supplied at offset '.$offset.' ('.$currentformatchar.').');
            }
        }

        return $result;
    }

    /**
     * binadd.
     *
     *  Convert a binary string to an array based on the given format string
     *
     * @param	$data   Data to convert to array
     *
     * @return array
     **/
    public function data2array($format, $data)
    {
        $dataarray = [];
        $dataarraykey = 0;
        $datakey = 0;
        $count = null;
        $loopcount = 0;
        $modifier = $this->MODIFIERS['@'];
        foreach (str_split($format) as $offset => $currentformatchar) {
            if (isset($this->MODIFIERS[$currentformatchar])) {
                $modifier = $this->MODIFIERS[$currentformatchar]; // Set the modifiers for the current format char
            } elseif (is_numeric($currentformatchar) && ((int) $currentformatchar > 0 || (int) $currentformatchar <= 9)) {
                $count .= $currentformatchar; // Set the count for the current format char
            } elseif (isset($modifier['SIZE'][$currentformatchar])) {
                if (!isset($count) || $count == null) {
                    $count = 1; // Set count to 1 if something's wrong.
                }
                $count = (int) $count;

                if ($currentformatchar == 's' || $currentformatchar == 'p') {
                    $loopcount = 1;
                } else {
                    $loopcount = $count;
                    $count = 1;
                }
                for ($x = 0; $x < $loopcount; $x++) {
                    if (!isset($dataarray[$dataarraykey])) {
                        $dataarray[$dataarraykey] = null;
                    }
                    for ($a = 0; $a < $count * $modifier['SIZE'][$currentformatchar]; $a++) {
                        $dataarray[$dataarraykey] .= $data[$datakey];
                        $datakey++;
                    }
                    $dataarraykey++;
                }
                $count = null;
            } else {
                throw new StructException('Unkown format or modifier supplied ('.$currentformatchar.' at offset '.$offset.').');
            }
        }

        return $dataarray;
    }

    /**
     * pdaformt.
     *
     *  Pad format string with x format where needed
     *
     * @param	$format Format string to pad
     *
     * @return Padded format string
     **/
    public function padformat($format)
    {
        $modifier = $this->MODIFIERS['@'];
        $result = null; // Result gormat string
        $count = null;
        $totallength = 0;
        foreach (str_split($format) as $offset => $currentformatchar) { // Current format char
            if (isset($this->MODIFIERS[$currentformatchar])) { // If current format char is a modifier
                $modifier = $this->MODIFIERS[$currentformatchar]; // Set the modifiers for the current format char
                $result = $currentformatchar;
            } elseif (is_numeric($currentformatchar) && ((int) $currentformatchar >= 0 || (int) $currentformatchar <= 9)) {
                $count .= (int) $currentformatchar; // Set the count for the current format char
            } elseif (isset($modifier['FORMATS'][$currentformatchar])) {
                if (!isset($count) || $count == null) {
                    $count = 1; // Set count to 1 by default.
                }
                $count = (int) $count;
                if ($currentformatchar == 's' || $currentformatchar == 'p') {
                    $result .= $count.$currentformatchar;
                    $totallength += $modifier['SIZE'][$currentformatchar] * $count;
                } else {
                    for ($x = 0; $x < $count; $x++) {
                        if ($modifier['MODIFIER'] == '@') {
                            $result .= str_pad('', $this->posmod(-$totallength, $modifier['SIZE'][$currentformatchar]), 'x');

                            $totallength += $this->posmod(-$totallength, $modifier['SIZE'][$currentformatchar]) + $modifier['SIZE'][$currentformatchar];
                        }
                        $result .= $currentformatchar;
                    }
                }
                $count = null;
            } else {
                throw new StructException('Unkown format or modifier supplied at offset '.$offset.' ('.$currentformatchar.').');
            }
        }

        return $result;
    }

    /**
     * decbin.
     *
     *  Returns a string containing a big endian binary representation of the given decimal number.
     *  Also pads binary number with zeros to match given $length
     *
     * @param	$number		Decimal number to turn into binary
     * @param	$length		Length to reach through padding
     *
     * @return binary version of the given number
     **/
    public function decbin($number, $length)
    {
        $concat = '';
        if ($number < 0) {
            $negative = true;
            $number = -$number;
        } else {
            $negative = false;
        }
        while ($number > 0) {
            $curchar = $this->posmod($number, 2);
            $concat = $curchar.$concat;
            $number = (($number - $curchar) / 2);
        }
        $concat = str_pad($concat, $length, '0', STR_PAD_LEFT);
        if ($negative) {
            $concat = $this->binadd($this->stringnot($concat), '1');
        }
        if (strlen($concat) == $length + 1 && $concat == str_pad('1', $length + 1, '0', STR_PAD_RIGHT)) {
            $concat = str_pad('', $length, '0');
        }
        if (strlen($concat) > $length) {
            trigger_error('Converted binary number '.$concat.' is too long ('.strlen($concat).' > '.$length.').');
        }

        return $concat;
    }

    /**
     * bindec.
     *
     *  Converts a binary number to a decimal.
     *
     * @param	$binary		binary number to turn into decimal
     * @param	$unsigned	if set to false will interpret binary string as signed
     *
     * @return decimal version of the given number
     **/
    public function bindec($binary, $unsigned = true)
    {
        $decimal = 0;
        if (!$unsigned && $binary[0] == '1') {
            $binary = $this->binadd($this->stringnot($binary), '1');
            $negative = true;
        } else {
            $negative = false;
        }

        foreach (str_split(strrev($binary)) as $n => $bit) {
            if ($bit == 1) {
                $decimal += pow(2, $n);
            }
        }

        return $negative ? -$decimal : $decimal;
    }

    /**
     * stringnot.
     *
     *  Performs a NOT operation on every bit in the string (by bit I mean a literal 1 or 0)
     *
     * @param	$string String to xor
     *
     * @return xored string
     **/
    public function stringnot($string)
    {
        foreach (str_split($string) as $key => $char) {
            if ($char == '0') {
                $string[$key] = '1';
            } elseif ($char == '1') {
                $string[$key] = '0';
            } else {
                trigger_error('Found unrecognized char '.$char.' at string offset '.$key);
            }
        }

        return $string;
    }

    /**
     * binadd.
     *
     *  Add two binary numbers
     *
     * @param	$x First binary number
     * @param	$y Second binary number
     *
     * @return sum of the two numbers
     **/
    public function binadd($x, $y)
    {
        $maxlen = max(strlen($x), strlen($y));

        //Normalize lengths
        $x = str_pad($x, $maxlen, '0', STR_PAD_LEFT);
        $y = str_pad($y, $maxlen, '0', STR_PAD_LEFT);

        $result = '';
        $carry = 0;
        for ($i = $maxlen - 1; $i >= 0; $i--) {
            $r = (int) ($carry + $x[$i] + $y[$i]);

            $result = ($r & 1).$result;
            $carry = $r >> 1;
        }
        if ($carry != 0) {
            $result = '1'.$result;
        }

        return str_pad($result, $maxlen, '0', STR_PAD_LEFT);
    }

    /**
     * num_pack.
     *
     * Convert a number to a big endian byte string.
     * If optional blocksize is given and greater than zero, pad the front of the
     * byte string with binary zeros so that the length is the
     * blocksize.
     *
     * @param	$n		    Number to pack
     * @param	$blocksize	Block size
     * @param   $unsigned Boolean that determines whether to work in signed or unsigned mode
     *
     * @return Byte string
     **/
    public function num_pack($n, $blocksize, $unsigned)
    {
        $bitnumber = $blocksize * 8;
        if ($unsigned) {
            $min = 0;
            switch ($bitnumber) {
                case '8':
                    $max = 255;
                    break;
                case '16':
                    $max = 65535;
                    break;
                case '32':
                    $max = 4294967295;
                    break;
                case '64':
                    $max = 18446744073709551615;
                    break;
                default:
                    $max = pow(2, $bitnumber) - 1;
                    break;
            }
        } else {
            switch ($bitnumber) {
                case '8':
                    $min = -128;
                    $max = 127;
                    break;
                case '16':
                    $min = -32768;
                    $max = 32767;
                    break;
                case '32':
                    $min = -2147483648;
                    $max = 2147483647;
                    break;
                case '64':
                    $min = -9223372036854775808;
                    $max = 9223372036854775807;
                    break;
                default:
                    $max = pow(2, $bitnumber - 1) - 1;
                    $min = -pow(2, $bitnumber - 1);
                    break;
            }
        }
        if ($n < $min || $n > $max) {
            trigger_error('Number is not within required range ('.$min.' <= number <= '.$max.').');
        }
        $bits = $this->decbin($n, $bitnumber);
        $s = '';
        foreach (str_split($bits, 8) as $byte) {
            $s .= chr($this->bindec($byte));
        }

        if (strlen($s) > $blocksize) {
            trigger_error('Generated data length ('.strlen($s).') is bigger than required length ('.$blocksize.').');
        }

        return $s;
    }

    /**
     * num_unpack.
     *
     * Convert a byte string to an integer.
     * This is (essentially) the inverse of num_pack().
     *
     * @param	$s		    Data to unpack
     * @param	$blocksize	Block size
     * @param   $unsigned Boolean that determines whether to work in signed or unsigned mode
     *
     * @return float or int with the unpack value
     **/
    public function num_unpack($s, $blocksize, $unsigned)
    {
        $length = strlen($s);
        $bitnumber = $blocksize * 8;
        if ($length != $blocksize) {
            trigger_error('Given data length ('.$length.') is different from the required length ('.$blocksize.').');
        }
        $bits = '';
        foreach (str_split($s) as $i) {
            $bits .= $this->decbin(ord($i), 8);
        }

        return $this->bindec($bits, $unsigned);
    }

    /**
     * posmod(numeric,numeric) : numeric
     * Works just like the % (modulus) operator, only returns always a postive number.
     */
    public function posmod($a, $b)
    {
        $resto = $a % $b;
        if ($resto < 0) {
            $resto += abs($b);
        }

        return $resto;
    }

    /**
     * array_each_strlen.
     *
     * Get length of each array element.
     *
     * @param	$array		Array to parse
     *
     * @return array with lengths
     **/
    public function array_each_strlen($array)
    {
        foreach ($array as &$value) {
            $value = $this->count($value);
        }

        return $array;
    }

    /**
     * array_total_strlen.
     *
     * Get total length of every array element.
     *
     * @param	$array		Array to parse
     *
     * @return int with the total length
     **/
    public function array_total_strlen($array)
    {
        $count = 0;
        foreach ($array as $value) {
            $count += $this->count($value);
        }

        return $count;
    }

    /**
     * count.
     *
     * Get the length of a string or of an array
     *
     * @param	$input		String or array to parse
     *
     * @return int with the length
     **/
    public function count($input)
    {
        if (is_array($input)) {
            return count($input);
        }

        return strlen($input);
    }
}


<?php

namespace danog\PHP;

/**
 * PHPStruct
 * PHP implementation of Python's struct module.
 * This library was created to help me develop a [client for the mtproto protocol](https://github.com/danog/MadelineProto).
 * The functions and the formats are exactly the ones used in python's struct (https://docs.python.org/3/library/struct.html)
 * For now custom byte size may not work properly on certain machines for the f and d formats.
 *
 * @author		Daniil Gentili <daniil@daniil.it>
 * @license		MIT license
 */
// Struct class (for static access)
class Struct
{
    private static $struct = null;

    /**
     * constructor.
     *
     * Istantiates the PHPStruct class in a static variable
     *
     * @param	$format	    Format string
     */
    public static function constructor()
    {
        if (self::$struct == null) {
            self::$struct = new \danog\PHP\StructTools();
        }
    }

    /**
     * pack.
     *
     * Packs data into bytes
     *
     * @param	$format	    Format string
     * @param	...$data	Parameters to encode
     *
     * @return Encoded data
     */
    public static function pack($format, ...$data)
    {
        self::constructor();

        return self::$struct->pack($format, ...$data);
    }

    /**
     * unpack.
     *
     * Unpacks data into an array
     *
     * @param	$format	Format string
     * @param	$data	Data to decode
     *
     * @return Decoded data
     */
    public static function unpack($format, $data)
    {
        self::constructor();

        return self::$struct->unpack($format, $data);
    }

    /**
     * calcsize.
     *
     * Return the size of the struct (and hence of the string) corresponding to the given format.
     *
     *
     * @param	$format	Format string
     *
     * @return int with size of the struct.
     */
    public static function calcsize($format)
    {
        self::constructor();

        return self::$struct->calcsize($format);
    }
}


<?php

namespace Dhii\EventManager;

use Psr\EventManager\EventInterface;

/**
 * Functionality for creating event instances.
 *
 * @since [*next-version*]
 */
trait CreateEventCapableTrait
{
    /**
     * Creates a new event instance.
     *
     * @since [*next-version*]
     *
     * @param string $name        The event name.
     * @param array  $params      The event parameters.
     * @param mixed  $target      The target object. Used for context.
     * @param bool   $propagation True to propagate the event, false to not.
     *
     * @return EventInterface The new event.
     */
    protected function _createEvent($name, $params = [], $target = null, $propagation = true)
    {
        return new Event($name, $params, $target, $propagation);
    }
}


<?php

namespace Dhii\EventManager;

use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;
use Psr\EventManager\EventInterface;
use Exception as RootException;

/**
 * Functionality for normalizing an event.
 *
 * @since [*next-version*]
 */
trait NormalizeEventCapableTrait
{
    /**
     * Normalizes the given event into an Event instance.
     *
     * @since [*next-version*]
     *
     * @param EventInterface|string|Stringable $event  Event instance or an event name string.
     * @param array                            $params The event parameters; will be added to existing parameters if
     *                                                 the $event argument is an {@see EventInterface} instance.
     * @param object                           $target The target of the event.
     *
     * @return EventInterface The event instance.
     */
    protected function _normalizeEvent($event, $params = [], $target = null)
    {
        if (is_string($event) || $event instanceof Stringable) {
            return $this->_createEvent($this->_normalizeString($event), $params, $target);
        }

        if (!($event instanceof EventInterface)) {
            throw $this->_createInvalidArgumentException(
                $this->__('Argument is not a string, stringable object or event instance'),
                null,
                null,
                $event
            );
        }

        if ($target !== null) {
            $event->setTarget($target);
        }

        if (count($params) > 0) {
            $event->setParams($params + $event->getParams());
        }

        return $event;
    }

    /**
     * Creates a new event instance.
     *
     * @since [*next-version*]
     *
     * @param string $name        The event name.
     * @param array  $params      The event parameters.
     * @param mixed  $target      The target object. Used for context.
     * @param bool   $propagation True to propagate the event, false to not.
     *
     * @return EventInterface The new event.
     */
    abstract protected function _createEvent($name, $params = [], $target = null, $propagation = true);

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);

    /**
     * Creates a new Dhii invalid argument exception.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|null $message  The error message, if any.
     * @param int|null               $code     The error code, if any.
     * @param RootException|null     $previous The inner exception for chaining, if any.
     * @param mixed|null             $argument The invalid argument, if any.
     *
     * @return InvalidArgumentException The new exception.
     */
    abstract protected function _createInvalidArgumentException(
        $message = null,
        $code = null,
        RootException $previous = null,
        $argument = null
    );

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see   sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = [], $context = null);
}


<?php

namespace Dhii\EventManager\WordPress;

use Closure;

/**
 * Provides functionality for retrieving, and creating on-request, WordPress event handler wrapper functions.
 *
 * @since [*next-version*]
 */
trait WpHandlerWrapperCacheTrait
{
    /**
     * A cache of WordPress handler wrappers, by hash.
     *
     * @since [*next-version*]
     *
     * @var callable[]
     */
    protected $handlerWrappers = [];

    /**
     * Gets a WordPress event handler wrapper.
     *
     * For the same name/callback pair, returns the same wrapper.
     *
     * @since [*next-version*]
     *
     * @param string   $name     Name of the event.
     * @param callable $callback Handler of the event.
     *
     * @return Closure The wrapper.
     */
    protected function _getWpHandlerWrapper($name, $callback)
    {
        $cbHash = $this->_hashWpHandler($name, $callback);

        // Create if not exists
        if (!isset($this->handlerWrappers[$cbHash])) {
            $this->handlerWrappers[$cbHash] = $this->_createWpHandlerWrapper(
                $name,
                $callback,
                $this->_getThrowOnPropStop()
            );
        }

        return $this->handlerWrappers[$cbHash];
    }

    /**
     * Hashes an event name / event handler pair.
     *
     * @since [*next-version*]
     *
     * @param string   $name    The name of the event.
     * @param callable $handler The handler of the event.
     *
     * @return string The hash of the pair.
     */
    abstract protected function _hashWpHandler($name, $handler);

    /**
     * Retrieves whether or not WP handler wrappers should throw stopped-propagation exceptions.
     *
     * @since [*next-version*]
     *
     * @return bool True if the WP handler wrappers should throw stopped-propagation exceptions, false if not.
     */
    abstract protected function _getThrowOnPropStop();

    /**
     * Creates a wrapper for a WordPress event handler, containing and replacing it.
     *
     * If the wrapper receives an {@see EventInterface}, it will use that and assume it was triggered and normalized by
     * a PSR-14 event manager.
     *
     * Otherwise it will assume that regular arguments have been passed (as would be done through the WordPress
     * functional API for hooks) and an event object will be created for those arguments.
     *
     * This consequently allows handlers to stop event propagation in WordPress. Profit!
     *
     * @since [*next-version*]
     *
     * @param string   $name            The name of the event, for which a wrapper is created.
     * @param callable $callback        The callback to wrap.
     * @param bool     $throwOnPropStop If true, a {@see StoppedPropagationExceptionInterface} exception
     *                                  is thrown when propagation is stopped.
     *
     * @return Closure The wrapper.
     */
    abstract protected function _createWpHandlerWrapper($name, $callback, $throwOnPropStop = false);
}


<?php

namespace Dhii\EventManager;

use Dhii\Util\String\StringableInterface as Stringable;
use Psr\EventManager\EventInterface;

/**
 * Functionality for caching event instances.
 *
 * @since [*next-version*]
 */
trait EventCacheTrait
{
    /**
     * A cache of event instances.
     *
     * @since [*next-version*]
     *
     * @var EventInterface[]
     */
    protected $eventCache;

    /**
     * Creates an event instance in cache.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|EventInterface $event The name of the event, or the event instance.
     * @param array                            $args  Optional array of event arguments.
     *
     * @return EventInterface The created event instance.
     */
    protected function _createCachedEvent($event, array $args = [])
    {
        $event = $this->_normalizeEvent($event, $args);
        $name  = $event->getName();

        return $this->eventCache[$name] = $event;
    }

    /**
     * Checks if an event instance exists in cache for the given event name.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|EventInterface $event The name of the event, or the event instance.
     *
     * @return bool True if an event instance exists for the given event name, false otherwise.
     */
    protected function _hasCachedEvent($event)
    {
        $name = ($event instanceof EventInterface)
            ? $event->getName()
            : $event;

        return isset($this->eventCache[$name]);
    }

    /**
     * Gets the cached event instance for the given name, creating it if needed.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|EventInterface $event The name of the event, or the event instance.
     * @param array                            $args  Optional array of event arguments.
     *
     * @return EventInterface The event instance.
     */
    protected function _getCachedEvent($event, array $args = [])
    {
        $name = ($event instanceof EventInterface)
            ? $event->getName()
            : $event;

        // Create event instance if it does not exist
        if (!$this->_hasCachedEvent($name)) {
            $this->_createCachedEvent($event, $args);
        }

        return $this->eventCache[$name];
    }

    /**
     * Removes a cached event instance.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|EventInterface $event The name of the event, or the event instance.
     *
     * @return $this This instance.
     */
    protected function _removeCachedEvent($event)
    {
        $name = ($event instanceof EventInterface)
            ? $event->getName()
            : $event;

        unset($this->eventCache[$name]);

        return $this;
    }

    /**
     * Normalizes the given event into an Event instance.
     *
     * @since [*next-version*]
     *
     * @param EventInterface|string|Stringable $event  Event instance or an event name string.
     * @param array                            $params The event parameters; will be added to existing parameters if
     *                                                 the $event argument is an {@see EventInterface} instance.
     * @param object                           $target The target of the event.
     *
     * @return EventInterface The event instance.
     */
    abstract protected function _normalizeEvent($event, $params = [], $target = null);
}


<?php

namespace Dhii\EventManager\WordPress;

use Closure;
use Dhii\EventManager\WordPress\Exception\StoppedPropagationExceptionInterface;
use Dhii\Util\String\StringableInterface as Stringable;
use Exception as RootException;
use Psr\EventManager\EventInterface;

/**
 * Functionality for creating WordPress handler wrappers.
 *
 * The wrappers created by this trait are compatible with both event managers and WordPress. By detecting whether the
 * handler was invoked with a single {@see EventInterface} instance or not, the handler can always return the
 * appropriate return value for the hook.
 *
 * @since [*next-version*]
 */
trait CreateWpHandlerWrapperCapableTrait
{
    /**
     * Creates a wrapper for a WordPress event handler, containing and replacing it.
     *
     * If the wrapper receives an {@see EventInterface}, it will use that and assume it was triggered and normalized by
     * a PSR-14 event manager.
     *
     * Otherwise it will assume that regular arguments have been passed (as would be done through the WordPress
     * functional API for hooks) and an event object will be created for those arguments.
     *
     * This consequently allows handlers to stop event propagation in WordPress. Profit!
     *
     * @since [*next-version*]
     *
     * @param string   $name            The name of the event, for which a wrapper is created.
     * @param callable $callback        The callback to wrap.
     * @param bool     $throwOnPropStop If true, a {@see StoppedPropagationExceptionInterface} exception
     *                                  is thrown when propagation is stopped.
     *
     * @return Closure The wrapper.
     */
    protected function _createWpHandlerWrapper($name, $callback, $throwOnPropStop = false)
    {
        $eventCache = $this;

        /*
         * $name - The name of the event
         * $callback - The actual handler callback
         * $eventCache - The callback for retrieving events from cache
         * $throwOnPropStop - If true, an exception is thrown when propagation is stopped.
         */

        return function () use ($name, &$callback, $eventCache, $throwOnPropStop) {
            $fnArgs = func_get_args();
            // Detect whether the first argument given to the handler is an EventInterface
            $firstArg = count($fnArgs) === 1
                ? $fnArgs[0]
                : null;

            if ($firstArg instanceof EventInterface) {
                // If first argument is an event instance, put in cache
                $event = $eventCache->_getCachedEvent($firstArg);
            } else {
                // Creates and retrieves an instance if an event name
                $event = $eventCache->_getCachedEvent($name, $fnArgs);
            }

            // Call original handler if propagation is not stopped
            if (!$event->isPropagationStopped()) {
                \call_user_func_array($callback, [$event]);
            }

            if ($event->isPropagationStopped() && $throwOnPropStop) {
                throw $this->_createStoppedPropagationException(
                    $this->__('Propagation has been stopped.'),
                    0,
                    null,
                    $event
                );
            }

            return $event->getParam(0);
        };
    }

    /**
     * Gets the cached event instance for the given name, creating it if needed.
     *
     * @since [*next-version*]
     *
     * @param string $name The name of the event.
     * @param array  $args Optional array of event arguments.
     *
     * @return EventInterface The event instance.
     */
    abstract protected function _getCachedEvent($name, array $args = []);

    /**
     * Creates a new exception for stopped event propagation.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|null $message  The error message, if any.
     * @param int|null               $code     The error code, if any.
     * @param RootException|null     $previous The previous exception for chaining, if any.
     * @param EventInterface|null    $event    The event that stopped, if any.
     *
     * @return StoppedPropagationExceptionInterface The created exception instance.
     */
    abstract protected function _createStoppedPropagationException(
        $message = null,
        $code = null,
        $previous = null,
        $event = null
    );

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see   sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = [], $context = null);
}


<?php

namespace Dhii\EventManager\WordPress;

use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;

/**
 * Functionality for removing handlers attached to WordPress hooks.
 *
 * @since [*next-version*]
 */
trait RemoveWpHookCapableTrait
{
    /**
     * Removes a hook handler from a WordPress event.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $name     The hook name.
     * @param callable          $handler  The hook handler callback.
     * @param int|null          $priority The priority of the hook - larger numbers signify later execution.
     */
    protected function _removeWpHook($name, callable $handler, $priority = null)
    {
        $name = $this->_normalizeString($name);

        $priority = ($priority === null)
            ? $this->_getWpHookDefaultPriority()
            : $this->_normalizeInt($priority);

        \remove_filter($name, $handler, $priority);
    }

    /**
     * Retrieves the default WordPress hook priority.
     *
     * Higher numbers should indicate later execution, and lower numbers indicate earlier execution.
     *
     * @since [*next-version*]
     *
     * @return int The default hook priority.
     */
    abstract protected function _getWpHookDefaultPriority();

    /**
     * Normalizes a value into an integer.
     *
     * The value must be a whole number, or a string representing such a number,
     * or an object representing such a string.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|float|int $value The value to normalize.
     *
     * @throws InvalidArgumentException If value cannot be normalized.
     *
     * @return int The normalized value.
     */
    abstract protected function _normalizeInt($value);

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);
}


<?php

namespace Dhii\EventManager\WordPress;

use WP_Hook;

/**
 * Functionality for replacing WordPress hook instances with replacement instances.
 *
 * @since [*next-version*]
 */
trait ReplaceWpHookCapableTrait
{
    /**
     * Replaces a WordPress hook instance.
     *
     * @since [*next-version*]
     *
     * @param string $hook The name of the hook.
     */
    protected function _replaceWpHook($hook)
    {
        global $wp_filter;

        if (!isset($wp_filter[$hook])) {
            return;
        }

        $wp_filter[$hook] = $this->_createWpHookReplacement($wp_filter[$hook]);
    }

    /**
     * Retrieves the replacement instance for the given WordPress hook.
     *
     * @since [*next-version*]
     *
     * @param WP_Hook $wpHook The WordPress hook instance.
     *
     * @return object The replacement instance.
     */
    abstract protected function _createWpHookReplacement($wpHook);
}


<?php

namespace Dhii\EventManager\WordPress;

use Dhii\Util\String\StringableInterface as Stringable;
use Exception as RootException;
use InvalidArgumentException;
use OutOfRangeException;

/**
 * Common functionality for awareness of the number of arguments to be passed to native WordPress hook handlers.
 *
 * @since [*next-version*]
 */
trait NumArgsAwareTrait
{
    /**
     * The argument count value to pass to the native WordPress action and filter hooking functions.
     *
     * @since [*next-version*]
     *
     * @var int
     */
    protected $numArgs;

    /**
     * Retrieves the argument count value to pass to the native WordPress action and filter hooking functions.
     *
     * @since [*next-version*]
     *
     * @return int The number of arguments.
     */
    protected function _getNumArgs()
    {
        return (int) $this->numArgs;
    }

    /**
     * Sets the argument count value to pass to the native WordPress action and filter hooking functions.
     *
     * @since [*next-version*]
     *
     * @param int $numArgs A positive argument count to pass to the native WordPress action and filter hooking
     *                     functions.
     */
    protected function _setNumArgs($numArgs)
    {
        $numArgs = $this->_normalizeInt($numArgs);

        if ($numArgs < 0) {
            throw $this->_createOutOfRangeException(
                $this->__('Number of arguments cannot be smaller than zero'), null, null, $numArgs
            );
        }

        $this->numArgs = $numArgs;
    }

    /**
     * Normalizes a value into an integer.
     *
     * The value must be a whole number, or a string representing such a number,
     * or an object representing such a string.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|float|int $value The value to normalize.
     *
     * @throws InvalidArgumentException If value cannot be normalized.
     *
     * @return int The normalized value.
     */
    abstract protected function _normalizeInt($value);

    /**
     * Creates a new Dhii Out Of Range exception.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|int|float|bool|null $message  The message, if any.
     * @param int|float|string|Stringable|null      $code     The numeric error code, if any.
     * @param RootException|null                    $previous The inner exception, if any.
     * @param mixed|null                            $argument The value that is out of range, if any.
     *
     * @return OutOfRangeException The new exception.
     */
    abstract protected function _createOutOfRangeException(
        $message = null,
        $code = null,
        RootException $previous = null,
        $argument = null
    );

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see   sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = [], $context = null);
}


<?php

namespace Dhii\EventManager\WordPress;

use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;

/**
 * Functionality for running the handlers attached to a WordPress hook.
 *
 * @since [*next-version*]
 */
trait RunWpHookCapableTrait
{
    /**
     * Causes WordPress to execute all handlers of the specified hook.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $name Name of the hook to run handlers for.
     * @param array             $args Arguments to pass to the handler.
     *
     * @return mixed The result returned by the last handler.
     */
    protected function _runWpHook($name, array $args = [])
    {
        $name = $this->_normalizeString($name);

        // Add the event name to the args array
        \array_unshift($args, $name);

        return \call_user_func_array('apply_filters', $args);
    }

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);
}


<?php

namespace Dhii\EventManager\WordPress;

use ArrayAccess;
use Dhii\EventManager\WordPress\Exception\StoppedPropagationExceptionInterface;
use Iterator;
use WP_Hook;

/**
 * A {@see WP_Hook} replacement wrapper class that detects {@see StoppedPropagationExceptionInterface} exceptions to
 * stop any further WordPress filters from invoking.
 *
 * @since [*next-version*]
 */
class WpHookReplacer implements Iterator, ArrayAccess
{
    /**
     * The original WP_Hook instance to proxy to.
     *
     * @since [*next-version*]
     *
     * @var WP_Hook
     */
    protected $wpHook;

    /**
     * Hook callbacks, pointing to the original.
     *
     * @var array
     */
    public $callbacks;

    /**
     * Constructor.
     *
     * @since [*next-version*]
     *
     * @param WP_Hook $wpHook The original WP_Hook instance,
     */
    public function __construct(WP_Hook $wpHook)
    {
        $this->wpHook    = $wpHook;
        $this->callbacks = &$wpHook->callbacks;
    }

    /**
     * Proxies method calls to the original hook instance.
     *
     * @since [*next-version*]
     *
     * @param string $name      The name of the method that was called.
     * @param array  $arguments The arguments given in the method call.
     *
     * @return mixed The return value.
     */
    public function __call($name, $arguments)
    {
        return call_user_func_array([$this->wpHook, $name], $arguments);
    }

    /**
     * Calls the callback functions added to a filter hook.
     *
     * This method is called instead of `$this->wpHook->apply_filters()`. This is done due to the fact that calls to
     * existing methods are not passed on to the `__call()` magic method.
     *
     * @param mixed $value The value to filter.
     * @param array $args  Arguments to pass to callbacks.
     *
     * @return mixed The filtered value after all hooked functions are applied to it.
     */
    public function apply_filters($value, $args)
    {
        try {
            $value = $this->wpHook->apply_filters($value, $args);
        } catch (StoppedPropagationExceptionInterface $stoppedPropagationException) {
            $event = $stoppedPropagationException->getEvent();
            $value = $event->getParam(0);
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function current()
    {
        return $this->wpHook->current();
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function next()
    {
        return $this->wpHook->next();
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function key()
    {
        return $this->wpHook->key();
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function valid()
    {
        return $this->wpHook->valid();
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function rewind()
    {
        return $this->wpHook->rewind();
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function offsetExists($offset)
    {
        return $this->wpHook->offsetExists($offset);
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function offsetGet($offset)
    {
        return $this->wpHook->offsetGet($offset);
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function offsetSet($offset, $value)
    {
        return $this->wpHook->offsetSet($offset, $value);
    }

    /**
     * {@inheritdoc}
     *
     * @since [*next-version*]
     */
    public function offsetUnset($offset)
    {
        return $this->wpHook->offsetUnset($offset);
    }
}


<?php

namespace Dhii\EventManager\WordPress;

use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;

/**
 * Functionality for registering WordPress hook handlers.
 *
 * @since [*next-version*]
 */
trait AddWpHookCapableTrait
{
    /**
     * Adds a hook handler to a WordPress event.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $name     The hook name.
     * @param callable          $handler  The hook handler callback.
     * @param int|null          $priority The priority of the hook - larger numbers signify later execution.
     * @param int|null          $numArgs  The number of arguments to accept from the event invoker.
     *
     * @throws InvalidArgumentException If the $name, $priority or $numArgs arguments are invalid.
     */
    protected function _addWpHook($name, callable $handler, $priority = null, $numArgs = 1)
    {
        $name = $this->_normalizeString($name);

        $priority = ($priority === null)
            ? $this->_getWpHookDefaultPriority()
            : $this->_normalizeInt($priority);

        $numArgs = ($numArgs === null)
            ? 1
            : $this->_normalizeInt($numArgs);

        \add_filter($name, $handler, $priority, $numArgs);
    }

    /**
     * Retrieves the default WordPress hook priority.
     *
     * Higher numbers should indicate later execution, and lower numbers indicate earlier execution.
     *
     * @since [*next-version*]
     *
     * @return int The default hook priority.
     */
    abstract protected function _getWpHookDefaultPriority();

    /**
     * Normalizes a value into an integer.
     *
     * The value must be a whole number, or a string representing such a number,
     * or an object representing such a string.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|float|int $value The value to normalize.
     *
     * @throws InvalidArgumentException If value cannot be normalized.
     *
     * @return int The normalized value.
     */
    abstract protected function _normalizeInt($value);

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);
}


<?php

namespace Dhii\EventManager\WordPress;

use Dhii\Util\String\StringableInterface as Stringable;
use InvalidArgumentException;
use ReflectionException;
use ReflectionMethod;

/**
 * Functionality for allowing objects to attach handlers for their own methods to an event.
 *
 * @since [*next-version*]
 */
trait AttachMethodHandlerCapableTrait
{
    /**
     * Attaches a handler for a method on this instance.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $eventName  The name of the event to attach to.
     * @param string|Stringable $methodName The name of the method to attach.
     * @param int|null          $priority   The priority to attach with.
     * @param int|null          $numArgs    The number of arguments to accept from the event invoker.
     *
     * @throws ReflectionException If a handler for the method could not be retrieved.
     */
    protected function _attachMethodHandler($eventName, $methodName, $priority = null, $numArgs = 1)
    {
        $methodName = $this->_normalizeString($methodName);
        $reflection = $this->_createReflectionMethod(get_class($this), $methodName);
        $handler    = $reflection->getClosure($this);

        $this->_addWpHook($eventName, $handler, $priority, $numArgs);
    }

    /**
     * Creates a new reflection for a method of a class.
     *
     * The function must exist.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $className  The class name.
     * @param string|Stringable $methodName The method name.
     *
     * @throws ReflectionException If the reflection could not be created.
     *
     * @return ReflectionMethod The new reflection.
     */
    abstract protected function _createReflectionMethod($className, $methodName);

    /**
     * Adds a hook handler to a WordPress event.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable $name     The hook name.
     * @param callable          $handler  The hook handler callback.
     * @param int|null          $priority The priority of the hook - larger numbers signify later execution.
     * @param int               $numArgs  The number of arguments to accept from the event invoker.
     */
    abstract protected function _addWpHook($name, callable $handler, $priority = null, $numArgs = 1);

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);
}


<?php

namespace Dhii\EventManager;

use Dhii\Util\String\StringableInterface as Stringable;
use Exception as RootException;
use InvalidArgumentException;
use OutOfRangeException;

/**
 * Functionality for generating hashes for all types of callables.
 *
 * @since [*next-version*]
 */
trait HashCallableCapableTrait
{
    /**
     * Computes a hash of a given callable.
     *
     * @since [*next-version*]
     *
     * @param callable|Stringable $callable The callable to hash.
     *
     * @throws OutOfRangeException      If the argument is not a valid callable.
     * @throws InvalidArgumentException If argument is a string/stringable and cannot be normalized.
     *
     * @return string A hash of the callable.
     */
    protected function _hashCallable($callable)
    {
        $key = null;

        if (is_string($callable) || $callable instanceof Stringable) {
            $key = $this->_normalizeString($callable);
        } elseif (is_object($callable)) {
            $key = spl_object_hash($callable);
        } elseif (is_array($callable) && count($callable) == 2) {
            $part1 = is_object($callable[0])
                ? spl_object_hash($callable[0])
                : $this->_normalizeString($callable[0]);
            $part2 = $this->_normalizeString($callable[1]);

            $key = serialize([$part1, $part2]);
        }

        if ($key === null) {
            throw $this->_createInvalidArgumentException(
                $this->__('Failed to hash - not a valid callable'),
                null,
                null,
                $callable
            );
        }

        return sha1($key);
    }

    /**
     * Normalizes a value to its string representation.
     *
     * The values that can be normalized are any scalar values, as well as
     * {@see StringableInterface).
     *
     * @since [*next-version*]
     *
     * @param Stringable|string|int|float|bool $subject The value to normalize to string.
     *
     * @throws InvalidArgumentException If the value cannot be normalized.
     *
     * @return string The string that resulted from normalization.
     */
    abstract protected function _normalizeString($subject);

    /**
     * Creates a new Dhii invalid argument exception.
     *
     * @since [*next-version*]
     *
     * @param string|Stringable|null $message  The error message, if any.
     * @param int|null               $code     The error code, if any.
     * @param RootException|null     $previous The inner exception for chaining, if any.
     * @param mixed|null             $argument The invalid argument, if any.
     *
     * @return InvalidArgumentException The new exception.
     */
    abstract protected function _createInvalidArgumentException(
        $message = null,
        $code = null,
        RootException $previous = null,
        $argument = null
    );

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see   sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = [], $context = null);
}