├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config.php └── src ├── Application ├── Application.php ├── ApplicationMixin.php ├── ApplicationServiceProvider.php ├── ApplicationTrait.php ├── ClosureFactory.php ├── GenericFactory.php ├── HasAliasesTrait.php ├── HasContainerTrait.php └── LoadsServiceProvidersTrait.php ├── Controllers ├── ControllersServiceProvider.php └── WordPressController.php ├── Csrf ├── Csrf.php ├── CsrfMiddleware.php ├── CsrfServiceProvider.php └── InvalidCsrfTokenException.php ├── Exceptions ├── ClassNotFoundException.php ├── ConfigurationException.php ├── ErrorHandler.php ├── ErrorHandlerInterface.php ├── Exception.php ├── ExceptionsServiceProvider.php └── Whoops │ ├── DebugDataProvider.php │ └── views │ ├── layout.html.php │ └── wpemerge-body.html.php ├── Flash ├── Flash.php ├── FlashMiddleware.php └── FlashServiceProvider.php ├── Helpers ├── Arguments.php ├── Handler.php ├── HandlerFactory.php ├── HasAttributesInterface.php ├── HasAttributesTrait.php ├── MixedType.php └── Url.php ├── Input ├── OldInput.php ├── OldInputMiddleware.php └── OldInputServiceProvider.php ├── Kernels ├── HttpKernel.php ├── HttpKernelInterface.php └── KernelsServiceProvider.php ├── Middleware ├── ControllerMiddleware.php ├── ExecutesMiddlewareTrait.php ├── HasControllerMiddlewareInterface.php ├── HasControllerMiddlewareTrait.php ├── HasMiddlewareDefinitionsInterface.php ├── HasMiddlewareDefinitionsTrait.php ├── MiddlewareServiceProvider.php ├── ReadsHandlerMiddlewareTrait.php ├── UserCanMiddleware.php ├── UserLoggedInMiddleware.php └── UserLoggedOutMiddleware.php ├── Requests ├── Request.php ├── RequestInterface.php └── RequestsServiceProvider.php ├── Responses ├── ConvertsToResponseTrait.php ├── RedirectResponse.php ├── ResponsableInterface.php ├── ResponseService.php └── ResponsesServiceProvider.php ├── Routing ├── Conditions │ ├── AdminCondition.php │ ├── AjaxCondition.php │ ├── CanFilterQueryInterface.php │ ├── ConditionFactory.php │ ├── ConditionInterface.php │ ├── CustomCondition.php │ ├── MultipleCondition.php │ ├── NegateCondition.php │ ├── PostIdCondition.php │ ├── PostSlugCondition.php │ ├── PostStatusCondition.php │ ├── PostTemplateCondition.php │ ├── PostTypeCondition.php │ ├── QueryVarCondition.php │ ├── UrlCondition.php │ └── UrlableInterface.php ├── HasQueryFilterInterface.php ├── HasQueryFilterTrait.php ├── HasRoutesInterface.php ├── HasRoutesTrait.php ├── NotFoundException.php ├── Route.php ├── RouteBlueprint.php ├── RouteInterface.php ├── Router.php ├── RoutingServiceProvider.php └── SortsMiddlewareTrait.php ├── ServiceProviders ├── ExtendsConfigTrait.php └── ServiceProviderInterface.php ├── Support └── Arr.php ├── View ├── HasContextInterface.php ├── HasContextTrait.php ├── HasNameTrait.php ├── NameProxyViewEngine.php ├── PhpView.php ├── PhpViewEngine.php ├── PhpViewFilesystemFinder.php ├── ViewEngineInterface.php ├── ViewException.php ├── ViewFinderInterface.php ├── ViewInterface.php ├── ViewNotFoundException.php ├── ViewService.php └── ViewServiceProvider.php └── view.php /README.md: -------------------------------------------------------------------------------- 1 | # WP Emerge Logo 2 | 3 | [![Packagist](https://img.shields.io/packagist/vpre/htmlburger/wpemerge.svg?style=flat-square&colorB=0366d6)](https://packagist.org/packages/htmlburger/wpemerge) [![Build](https://img.shields.io/github/workflow/status/htmlburger/wpemerge/PHPUnit%20Tests?style=flat-square)](https://github.com/htmlburger/wpemerge/actions/workflows/phpunit.yml) [![Scrutinizer](https://img.shields.io/scrutinizer/g/htmlburger/wpemerge.svg?style=flat-square)](https://scrutinizer-ci.com/g/htmlburger/wpemerge/) [![Scrutinizer Coverage](https://img.shields.io/scrutinizer/coverage/g/htmlburger/wpemerge.svg?style=flat-square)](https://scrutinizer-ci.com/g/htmlburger/wpemerge/code-structure/master/code-coverage) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square&colorB=7d07d1)](https://gitter.im/wpemerge/Lobby) 4 | 5 | 📦 A micro framework which modernizes WordPress as a CMS development by providing tools to implement MVC and more. 6 | 7 | 🔥 Integration is incremental - you can use it only on the pages you want or on all pages. 8 | 9 | ❤ If you've used frameworks such as Laravel, Slim or Symfony you will love WP Emerge. 10 | 11 | 🚀 Also, make sure you check out the WP Emerge [Starter Plugin](https://github.com/htmlburger/wpemerge-plugin) and [Starter Theme](https://github.com/htmlburger/wpemerge-theme) projects. 12 | 13 | ## Requirements 14 | 15 | - [PHP](http://php.net/) >= 5.5 16 | - [WordPress](https://wordpress.org/) >= 4.7 17 | - [Composer](https://getcomposer.org/) 18 | 19 | ## Features & Documentation 20 | 21 | [https://docs.wpemerge.com/#/framework/overview](https://docs.wpemerge.com/#/framework/overview) 22 | 23 | [https://docs.wpemerge.com/#/framework/quickstart](https://docs.wpemerge.com/#/framework/quickstart) 24 | 25 | ## API Reference 26 | 27 | [https://api.wpemerge.com/](https://api.wpemerge.com/) 28 | 29 | ## Development Team 30 | 31 | Brought to you by [Atanas Angelov](https://atanas.dev/) and the lovely folks at [htmlBurger](http://htmlburger.com). 32 | 33 | ## Contributing 34 | 35 | WP Emerge is completely open source and we encourage everybody to participate by: 36 | 37 | - Reviewing `.github/CONTRIBUTING.md`. 38 | - ⭐ the project on GitHub \([https://github.com/htmlburger/wpemerge](https://github.com/htmlburger/wpemerge)\) 39 | - Posting bug reports \([https://github.com/htmlburger/wpemerge/issues](https://github.com/htmlburger/wpemerge/issues)\) 40 | - (Emailing security issues to [hi@atanas.dev](mailto:hi@atanas.dev) instead) 41 | - Posting feature suggestions \([https://github.com/htmlburger/wpemerge/issues](https://github.com/htmlburger/wpemerge/issues)\) 42 | - Posting and/or answering questions \([https://github.com/htmlburger/wpemerge/issues](https://github.com/htmlburger/wpemerge/issues)\) 43 | - Submitting pull requests \([https://github.com/htmlburger/wpemerge/pulls](https://github.com/htmlburger/wpemerge/pulls)\) 44 | - Sharing your excitement about WP Emerge with your community 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmlburger/wpemerge", 3 | "description": "A micro framework which modernizes WordPress as a CMS development by providing tools to implement MVC and more.", 4 | "type": "library", 5 | "keywords": ["wordpress", "framework", "controller", "template", "view", "request", "response", "middleware", "wpemerge"], 6 | "license": "GPL-2.0-only", 7 | "homepage": "https://wpemerge.com/", 8 | "authors": [ 9 | { 10 | "name": "Atanas Angelov", 11 | "email": "hi@atanas.dev", 12 | "homepage": "https://atanas.dev/", 13 | "role": "Developer" 14 | }, 15 | { 16 | "name": "htmlBurger", 17 | "email": "info@htmlburger.com", 18 | "homepage": "http://htmlburger.com/", 19 | "role": "Developer" 20 | } 21 | ], 22 | "support": { 23 | "source": "https://github.com/htmlburger/wpemerge", 24 | "issues": "https://github.com/htmlburger/wpemerge/issues", 25 | "email": "wordpress@htmlburger.com" 26 | }, 27 | "require": { 28 | "php": ">=5.5", 29 | "pimple/pimple": "^3.2", 30 | "guzzlehttp/psr7": "^1.5", 31 | "psr/container": "^1.0.0" 32 | }, 33 | "require-dev": { 34 | "filp/whoops": "^2.2", 35 | "squizlabs/php_codesniffer": "^3.3", 36 | "phpcompatibility/php-compatibility": "^9.0", 37 | "mockery/mockery": "^0.9.11|~1.3.2" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "WPEmerge\\": "src/", 42 | "WPEmergeTestTools\\": "tests/tools" 43 | }, 44 | "files": [ 45 | "config.php" 46 | ] 47 | }, 48 | "scripts": { 49 | "lint": "phpcs", 50 | "install-test-env": "WPEMERGE_PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION . \".\" . PHP_MINOR_VERSION;') && if [ \"$WPEMERGE_PHP_VER\" \\> \"7.1\" ] && [ \"$WPEMERGE_PHP_VER\" \\< \"8.0\" ]; then composer require --dev phpunit/phpunit:^7 yoast/phpunit-polyfills; else composer require --dev phpunit/phpunit yoast/phpunit-polyfills; fi", 51 | "test": "phpunit --coverage-clover=./tmp/clover.xml" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | /** 11 | * Current version. 12 | */ 13 | if ( ! defined( 'WPEMERGE_VERSION' ) ) { 14 | define( 'WPEMERGE_VERSION', '0.17.0' ); 15 | } 16 | 17 | /** 18 | * Absolute path to application's directory. 19 | */ 20 | if ( ! defined( 'WPEMERGE_DIR' ) ) { 21 | define( 'WPEMERGE_DIR', __DIR__ ); 22 | } 23 | 24 | /** 25 | * Service container keys. 26 | */ 27 | if ( ! defined( 'WPEMERGE_CONFIG_KEY' ) ) { 28 | define( 'WPEMERGE_CONFIG_KEY', 'wpemerge.config' ); 29 | } 30 | 31 | if ( ! defined( 'WPEMERGE_APPLICATION_KEY' ) ) { 32 | define( 'WPEMERGE_APPLICATION_KEY', 'wpemerge.application.application' ); 33 | } 34 | 35 | if ( ! defined( 'WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY' ) ) { 36 | define( 'WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY', 'wpemerge.application.generic_factory' ); 37 | } 38 | 39 | if ( ! defined( 'WPEMERGE_APPLICATION_CLOSURE_FACTORY_KEY' ) ) { 40 | define( 'WPEMERGE_APPLICATION_CLOSURE_FACTORY_KEY', 'wpemerge.application.closure_factory' ); 41 | } 42 | 43 | if ( ! defined( 'WPEMERGE_APPLICATION_FILESYSTEM_KEY' ) ) { 44 | define( 'WPEMERGE_APPLICATION_FILESYSTEM_KEY', 'wpemerge.application.filesystem' ); 45 | } 46 | 47 | if ( ! defined( 'WPEMERGE_HELPERS_HANDLER_FACTORY_KEY' ) ) { 48 | define( 'WPEMERGE_HELPERS_HANDLER_FACTORY_KEY', 'wpemerge.handlers.helper_factory' ); 49 | } 50 | 51 | if ( ! defined( 'WPEMERGE_WORDPRESS_HTTP_KERNEL_KEY' ) ) { 52 | define( 'WPEMERGE_WORDPRESS_HTTP_KERNEL_KEY', 'wpemerge.kernels.wordpress_http_kernel' ); 53 | } 54 | 55 | if ( ! defined( 'WPEMERGE_SESSION_KEY' ) ) { 56 | define( 'WPEMERGE_SESSION_KEY', 'wpemerge.session' ); 57 | } 58 | 59 | if ( ! defined( 'WPEMERGE_REQUEST_KEY' ) ) { 60 | define( 'WPEMERGE_REQUEST_KEY', 'wpemerge.request' ); 61 | } 62 | 63 | if ( ! defined( 'WPEMERGE_RESPONSE_KEY' ) ) { 64 | define( 'WPEMERGE_RESPONSE_KEY', 'wpemerge.response' ); 65 | } 66 | 67 | if ( ! defined( 'WPEMERGE_EXCEPTIONS_ERROR_HANDLER_KEY' ) ) { 68 | define( 'WPEMERGE_EXCEPTIONS_ERROR_HANDLER_KEY', 'wpemerge.exceptions.error_handler' ); 69 | } 70 | 71 | if ( ! defined( 'WPEMERGE_EXCEPTIONS_CONFIGURATION_ERROR_HANDLER_KEY' ) ) { 72 | define( 'WPEMERGE_EXCEPTIONS_CONFIGURATION_ERROR_HANDLER_KEY', 'wpemerge.exceptions.configuration_error_handler' ); 73 | } 74 | 75 | if ( ! defined( 'WPEMERGE_RESPONSE_SERVICE_KEY' ) ) { 76 | define( 'WPEMERGE_RESPONSE_SERVICE_KEY', 'wpemerge.responses.response_service' ); 77 | } 78 | 79 | if ( ! defined( 'WPEMERGE_ROUTING_ROUTER_KEY' ) ) { 80 | define( 'WPEMERGE_ROUTING_ROUTER_KEY', 'wpemerge.routing.router' ); 81 | } 82 | 83 | if ( ! defined( 'WPEMERGE_ROUTING_ROUTE_BLUEPRINT_KEY' ) ) { 84 | define( 'WPEMERGE_ROUTING_ROUTE_BLUEPRINT_KEY', 'wpemerge.routing.route_registrar' ); 85 | } 86 | 87 | if ( ! defined( 'WPEMERGE_ROUTING_CONDITIONS_CONDITION_FACTORY_KEY' ) ) { 88 | define( 'WPEMERGE_ROUTING_CONDITIONS_CONDITION_FACTORY_KEY', 'wpemerge.routing.conditions.condition_factory' ); 89 | } 90 | 91 | if ( ! defined( 'WPEMERGE_ROUTING_CONDITION_TYPES_KEY' ) ) { 92 | define( 'WPEMERGE_ROUTING_CONDITION_TYPES_KEY', 'wpemerge.routing.conditions.condition_types' ); 93 | } 94 | 95 | if ( ! defined( 'WPEMERGE_VIEW_SERVICE_KEY' ) ) { 96 | define( 'WPEMERGE_VIEW_SERVICE_KEY', 'wpemerge.view.view_service' ); 97 | } 98 | 99 | if ( ! defined( 'WPEMERGE_VIEW_COMPOSE_ACTION_KEY' ) ) { 100 | define( 'WPEMERGE_VIEW_COMPOSE_ACTION_KEY', 'wpemerge.view.view_compose_action' ); 101 | } 102 | 103 | if ( ! defined( 'WPEMERGE_VIEW_ENGINE_KEY' ) ) { 104 | define( 'WPEMERGE_VIEW_ENGINE_KEY', 'wpemerge.view.view_engine' ); 105 | } 106 | 107 | if ( ! defined( 'WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY' ) ) { 108 | define( 'WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY', 'wpemerge.view.php_view_engine' ); 109 | } 110 | 111 | if ( ! defined( 'WPEMERGE_SERVICE_PROVIDERS_KEY' ) ) { 112 | define( 'WPEMERGE_SERVICE_PROVIDERS_KEY', 'wpemerge.service_providers' ); 113 | } 114 | 115 | if ( ! defined( 'WPEMERGE_FLASH_KEY' ) ) { 116 | define( 'WPEMERGE_FLASH_KEY', 'wpemerge.flash.flash' ); 117 | } 118 | 119 | if ( ! defined( 'WPEMERGE_OLD_INPUT_KEY' ) ) { 120 | define( 'WPEMERGE_OLD_INPUT_KEY', 'wpemerge.old_input.old_input' ); 121 | } 122 | 123 | if ( ! defined( 'WPEMERGE_CSRF_KEY' ) ) { 124 | define( 'WPEMERGE_CSRF_KEY', 'wpemerge.csrf.csrf' ); 125 | } 126 | -------------------------------------------------------------------------------- /src/Application/Application.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Closure; 13 | use Pimple\Container; 14 | use WPEmerge\Exceptions\ConfigurationException; 15 | use WPEmerge\Requests\Request; 16 | use WPEmerge\Support\Arr; 17 | 18 | /** 19 | * The core WP Emerge component representing an application. 20 | */ 21 | class Application { 22 | use HasAliasesTrait; 23 | use LoadsServiceProvidersTrait; 24 | use HasContainerTrait; 25 | 26 | /** 27 | * Flag whether to intercept and render configuration exceptions. 28 | * 29 | * @var boolean 30 | */ 31 | protected $render_config_exceptions = true; 32 | 33 | /** 34 | * Flag whether the application has been bootstrapped. 35 | * 36 | * @var boolean 37 | */ 38 | protected $bootstrapped = false; 39 | 40 | /** 41 | * Make a new application instance. 42 | * 43 | * @codeCoverageIgnore 44 | * @return static 45 | */ 46 | public static function make() { 47 | return new static( new Container() ); 48 | } 49 | 50 | /** 51 | * Constructor. 52 | * 53 | * @param Container $container 54 | * @param boolean $render_config_exceptions 55 | */ 56 | public function __construct( Container $container, $render_config_exceptions = true ) { 57 | $this->setContainer( $container ); 58 | $this->container()[ WPEMERGE_APPLICATION_KEY ] = $this; 59 | $this->render_config_exceptions = $render_config_exceptions; 60 | } 61 | 62 | /** 63 | * Get whether the application has been bootstrapped. 64 | * 65 | * @return boolean 66 | */ 67 | public function isBootstrapped() { 68 | return $this->bootstrapped; 69 | } 70 | 71 | /** 72 | * Bootstrap the application. 73 | * 74 | * @param array $config 75 | * @param boolean $run 76 | * @return void 77 | */ 78 | public function bootstrap( $config = [], $run = true ) { 79 | if ( $this->isBootstrapped() ) { 80 | throw new ConfigurationException( static::class . ' already bootstrapped.' ); 81 | } 82 | 83 | $this->bootstrapped = true; 84 | 85 | $container = $this->container(); 86 | $this->loadConfig( $container, $config ); 87 | $this->loadServiceProviders( $container ); 88 | 89 | $this->renderConfigExceptions( function () use ( $run ) { 90 | $this->loadRoutes(); 91 | 92 | if ( $run ) { 93 | $kernel = $this->resolve( WPEMERGE_WORDPRESS_HTTP_KERNEL_KEY ); 94 | $kernel->bootstrap(); 95 | } 96 | } ); 97 | } 98 | 99 | /** 100 | * Load config into the service container. 101 | * 102 | * @codeCoverageIgnore 103 | * @param Container $container 104 | * @param array $config 105 | * @return void 106 | */ 107 | protected function loadConfig( Container $container, $config ) { 108 | $container[ WPEMERGE_CONFIG_KEY ] = $config; 109 | } 110 | 111 | /** 112 | * Load route definition files depending on the current request. 113 | * 114 | * @codeCoverageIgnore 115 | * @return void 116 | */ 117 | protected function loadRoutes() { 118 | if ( wp_doing_ajax() ) { 119 | $this->loadRoutesGroup( 'ajax' ); 120 | return; 121 | } 122 | 123 | if ( is_admin() ) { 124 | $this->loadRoutesGroup( 'admin' ); 125 | return; 126 | } 127 | 128 | $this->loadRoutesGroup( 'web' ); 129 | } 130 | 131 | /** 132 | * Load a route group applying default attributes, if any. 133 | * 134 | * @codeCoverageIgnore 135 | * @param string $group 136 | * @return void 137 | */ 138 | protected function loadRoutesGroup( $group ) { 139 | $config = $this->resolve( WPEMERGE_CONFIG_KEY ); 140 | $file = Arr::get( $config, 'routes.' . $group . '.definitions', '' ); 141 | $attributes = Arr::get( $config, 'routes.' . $group . '.attributes', [] ); 142 | 143 | if ( empty( $file ) ) { 144 | return; 145 | } 146 | 147 | $middleware = Arr::get( $attributes, 'middleware', [] ); 148 | 149 | if ( ! in_array( $group, $middleware, true ) ) { 150 | $middleware = array_merge( [$group], $middleware ); 151 | } 152 | 153 | $attributes['middleware'] = $middleware; 154 | 155 | $blueprint = $this->resolve( WPEMERGE_ROUTING_ROUTE_BLUEPRINT_KEY ); 156 | $blueprint->attributes( $attributes )->group( $file ); 157 | } 158 | 159 | /** 160 | * Catch any configuration exceptions and short-circuit to an error page. 161 | * 162 | * @codeCoverageIgnore 163 | * @param Closure $action 164 | * @return void 165 | */ 166 | public function renderConfigExceptions( Closure $action ) { 167 | try { 168 | $action(); 169 | } catch ( ConfigurationException $exception ) { 170 | if ( ! $this->render_config_exceptions ) { 171 | throw $exception; 172 | } 173 | 174 | $request = Request::fromGlobals(); 175 | $handler = $this->resolve( WPEMERGE_EXCEPTIONS_CONFIGURATION_ERROR_HANDLER_KEY ); 176 | 177 | add_filter( 'wpemerge.pretty_errors.apply_admin_styles', '__return_false' ); 178 | 179 | $response_service = $this->resolve( WPEMERGE_RESPONSE_SERVICE_KEY ); 180 | $response_service->respond( $handler->getResponse( $request, $exception ) ); 181 | 182 | wp_die(); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Application/ApplicationMixin.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Pimple\Container; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Requests\RequestInterface; 15 | use WPEmerge\Responses\RedirectResponse; 16 | use WPEmerge\Routing\RouteBlueprint; 17 | use WPEmerge\View\ViewInterface; 18 | 19 | /** 20 | * Can be applied to your App class via a "@mixin" annotation for better IDE support. 21 | * This class is not meant to be used in any other capacity. 22 | * 23 | * @codeCoverageIgnore 24 | */ 25 | final class ApplicationMixin { 26 | /** 27 | * Prevent class instantiation. 28 | */ 29 | private function __construct() {} 30 | 31 | // --- Methods --------------------------------------- // 32 | 33 | /** 34 | * Get whether the application has been bootstrapped. 35 | * 36 | * @return boolean 37 | */ 38 | public static function isBootstrapped() {} 39 | 40 | /** 41 | * Bootstrap the application. 42 | * 43 | * @param array $config 44 | * @param boolean $run 45 | * @return void 46 | */ 47 | public static function bootstrap( $config = [], $run = true ) {} 48 | 49 | /** 50 | * Get the IoC container instance. 51 | * 52 | * @codeCoverageIgnore 53 | * @return Container 54 | */ 55 | public static function container() {} 56 | 57 | /** 58 | * Set the IoC container instance. 59 | * 60 | * @codeCoverageIgnore 61 | * @param Container $container 62 | * @return void 63 | */ 64 | public static function setContainer( $container ) {} 65 | 66 | /** 67 | * Resolve a dependency from the IoC container. 68 | * 69 | * @param string $key 70 | * @return mixed|null 71 | */ 72 | public static function resolve( $key ) {} 73 | 74 | // --- Aliases --------------------------------------- // 75 | 76 | /** 77 | * Get the Application instance. 78 | * 79 | * @codeCoverageIgnore 80 | * @return \WPEmerge\Application\Application 81 | */ 82 | public static function app() {} 83 | 84 | /** 85 | * Get the ClosureFactory instance. 86 | * 87 | * @codeCoverageIgnore 88 | * @return ClosureFactory 89 | */ 90 | public static function closure() {} 91 | 92 | /** 93 | * Get the CSRF service instance. 94 | * 95 | * @codeCoverageIgnore 96 | * @return \WPEmerge\Csrf\Csrf 97 | */ 98 | public static function csrf() {} 99 | 100 | /** 101 | * Get the Flash service instance. 102 | * 103 | * @codeCoverageIgnore 104 | * @return \WPEmerge\Flash\Flash 105 | */ 106 | public static function flash() {} 107 | 108 | /** 109 | * Get the OldInput service instance. 110 | * 111 | * @codeCoverageIgnore 112 | * @return \WPEmerge\Input\OldInput 113 | */ 114 | public static function oldInput() {} 115 | 116 | /** 117 | * Run a full middleware + handler pipeline independently of routes. 118 | * 119 | * @codeCoverageIgnore 120 | * @see \WPEmerge\Kernels\HttpKernel::run() 121 | * @param RequestInterface $request 122 | * @param string[] $middleware 123 | * @param string|\Closure $handler 124 | * @param array $arguments 125 | * @return ResponseInterface 126 | */ 127 | public static function run( RequestInterface $request, $middleware, $handler, $arguments = [] ) {} 128 | 129 | /** 130 | * Get the ResponseService instance. 131 | * 132 | * @codeCoverageIgnore 133 | * @return \WPEmerge\Responses\ResponseService 134 | */ 135 | public static function responses() {} 136 | 137 | /** 138 | * Create a "blank" response. 139 | * 140 | * @codeCoverageIgnore 141 | * @see \WPEmerge\Responses\ResponseService::response() 142 | * @return ResponseInterface 143 | */ 144 | public static function response() {} 145 | 146 | /** 147 | * Create a response with the specified string as its body. 148 | * 149 | * @codeCoverageIgnore 150 | * @see \WPEmerge\Responses\ResponseService::output() 151 | * @param string $output 152 | * @return ResponseInterface 153 | */ 154 | public static function output( $output ) {} 155 | 156 | /** 157 | * Create a response with the specified data encoded as JSON as its body. 158 | * 159 | * @codeCoverageIgnore 160 | * @see \WPEmerge\Responses\ResponseService::json() 161 | * @param mixed $data 162 | * @return ResponseInterface 163 | */ 164 | public static function json( $data ) {} 165 | 166 | /** 167 | * Create a redirect response. 168 | * 169 | * @codeCoverageIgnore 170 | * @see \WPEmerge\Responses\ResponseService::redirect() 171 | * @return RedirectResponse 172 | */ 173 | public static function redirect() {} 174 | 175 | /** 176 | * Create a response with the specified error status code. 177 | * 178 | * @codeCoverageIgnore 179 | * @see \WPEmerge\Responses\ResponseService::error() 180 | * @param integer $status 181 | * @return ResponseInterface 182 | */ 183 | public static function error( $status ) {} 184 | 185 | /** 186 | * Get the ViewService instance. 187 | * 188 | * @codeCoverageIgnore 189 | * @return \WPEmerge\View\ViewService 190 | */ 191 | public static function views() {} 192 | 193 | /** 194 | * Create a view. 195 | * 196 | * @codeCoverageIgnore 197 | * @see \WPEmerge\View\ViewService::make() 198 | * @param string|string[] $views 199 | * @return ViewInterface 200 | */ 201 | public static function view( $views ) {} 202 | 203 | /** 204 | * Output child layout content. 205 | * 206 | * @codeCoverageIgnore 207 | * @see \WPEmerge\View\PhpViewEngine::getLayoutContent() 208 | * @return void 209 | */ 210 | public static function layoutContent() {} 211 | 212 | /** 213 | * Create a new route. 214 | * 215 | * @codeCoverageIgnore 216 | * @return RouteBlueprint 217 | */ 218 | public static function route() {} 219 | 220 | /** 221 | * Output the specified view. 222 | * 223 | * @codeCoverageIgnore 224 | * @see \WPEmerge\View\ViewService::make() 225 | * @see \WPEmerge\View\ViewInterface::toString() 226 | * @param string|string[] $views 227 | * @param array $context 228 | * @return void 229 | */ 230 | public static function render( $views, $context = [] ) {} 231 | } 232 | -------------------------------------------------------------------------------- /src/Application/ApplicationServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use WPEmerge\Helpers\HandlerFactory; 13 | use WPEmerge\Helpers\MixedType; 14 | use WPEmerge\ServiceProviders\ExtendsConfigTrait; 15 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 16 | 17 | /** 18 | * Provide application dependencies. 19 | * 20 | * @codeCoverageIgnore 21 | */ 22 | class ApplicationServiceProvider implements ServiceProviderInterface { 23 | use ExtendsConfigTrait; 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function register( $container ) { 29 | $this->extendConfig( $container, 'providers', [] ); 30 | $this->extendConfig( $container, 'namespace', 'App\\' ); 31 | 32 | $upload_dir = wp_upload_dir(); 33 | $cache_dir = MixedType::addTrailingSlash( $upload_dir['basedir'] ) . 'wpemerge' . DIRECTORY_SEPARATOR . 'cache'; 34 | $this->extendConfig( $container, 'cache', [ 35 | 'path' => $cache_dir, 36 | ] ); 37 | 38 | $container[ WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY ] = function ( $c ) { 39 | return new GenericFactory( $c ); 40 | }; 41 | 42 | $container[ WPEMERGE_APPLICATION_CLOSURE_FACTORY_KEY ] = function ( $c ) { 43 | return new ClosureFactory( $c[ WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY ] ); 44 | }; 45 | 46 | $container[ WPEMERGE_HELPERS_HANDLER_FACTORY_KEY ] = function ( $c ) { 47 | return new HandlerFactory( $c[ WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY ] ); 48 | }; 49 | 50 | $container[ WPEMERGE_APPLICATION_FILESYSTEM_KEY ] = function ( $c ) { 51 | global $wp_filesystem; 52 | 53 | require_once ABSPATH . '/wp-admin/includes/file.php'; 54 | 55 | WP_Filesystem(); 56 | 57 | return $wp_filesystem; 58 | }; 59 | 60 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 61 | $app->alias( 'app', WPEMERGE_APPLICATION_KEY ); 62 | $app->alias( 'closure', WPEMERGE_APPLICATION_CLOSURE_FACTORY_KEY ); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function bootstrap( $container ) { 69 | $cache_dir = $container[ WPEMERGE_CONFIG_KEY ]['cache']['path']; 70 | wp_mkdir_p( $cache_dir ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Application/ApplicationTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use WPEmerge\Exceptions\ConfigurationException; 13 | 14 | /** 15 | * Provides static access to an Application instance. 16 | * 17 | * @mixin ApplicationMixin 18 | */ 19 | trait ApplicationTrait { 20 | /** 21 | * Application instance. 22 | * 23 | * @var Application|null 24 | */ 25 | public static $instance = null; 26 | 27 | /** 28 | * Make and assign a new application instance. 29 | * 30 | * @return Application 31 | */ 32 | public static function make() { 33 | static::setApplication( Application::make() ); 34 | 35 | return static::getApplication(); 36 | } 37 | 38 | /** 39 | * Get the Application instance. 40 | * 41 | * @codeCoverageIgnore 42 | * @return Application|null 43 | */ 44 | public static function getApplication() { 45 | return static::$instance; 46 | } 47 | 48 | /** 49 | * Set the Application instance. 50 | * 51 | * @codeCoverageIgnore 52 | * @param Application|null $application 53 | * @return void 54 | */ 55 | public static function setApplication( $application ) { 56 | static::$instance = $application; 57 | } 58 | 59 | /** 60 | * Invoke any matching instance method for the static method being called. 61 | * 62 | * @param string $method 63 | * @param array $parameters 64 | * @return mixed 65 | */ 66 | public static function __callStatic( $method, $parameters ) { 67 | $application = static::getApplication(); 68 | 69 | if ( ! $application ) { 70 | throw new ConfigurationException( 71 | 'Application instance not created in ' . static::class . '. ' . 72 | 'Did you miss to call ' . static::class . '::make()?' 73 | ); 74 | } 75 | 76 | return call_user_func_array( [$application, $method], $parameters ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Application/ClosureFactory.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Closure; 13 | 14 | /** 15 | * Factory that makes closures which resolve values from the container. 16 | */ 17 | class ClosureFactory { 18 | /** 19 | * Factory. 20 | * 21 | * @var GenericFactory 22 | */ 23 | protected $factory = null; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @codeCoverageIgnore 29 | * @param GenericFactory $factory 30 | */ 31 | public function __construct( GenericFactory $factory ) { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * Make a closure that resolves a value from the container. 37 | * 38 | * @param string $key 39 | * @return Closure 40 | */ 41 | public function value( $key ) { 42 | return function () use ( $key ) { 43 | return $this->factory->make( $key ); 44 | }; 45 | } 46 | 47 | /** 48 | * Make a closure that resolves a class instance from the container and 49 | * calls one of its methods. 50 | * Useful if you need to pass a callable to an API without container 51 | * support such as the REST API. 52 | * 53 | * @param string $key 54 | * @param string $method 55 | * @return Closure 56 | */ 57 | public function method( $key, $method ) { 58 | return function () use ( $key, $method ) { 59 | return call_user_func_array( 60 | [$this->factory->make( $key ), $method], 61 | func_get_args() 62 | ); 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Application/GenericFactory.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Pimple\Container; 13 | use WPEmerge\Exceptions\ClassNotFoundException; 14 | 15 | /** 16 | * Generic class instance factory. 17 | */ 18 | class GenericFactory { 19 | /** 20 | * Container. 21 | * 22 | * @var Container 23 | */ 24 | protected $container = null; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @codeCoverageIgnore 30 | * @param Container $container 31 | */ 32 | public function __construct( Container $container ) { 33 | $this->container = $container; 34 | } 35 | 36 | /** 37 | * Make a class instance. 38 | * 39 | * @throws ClassNotFoundException 40 | * @param string $class 41 | * @return object 42 | */ 43 | public function make( $class ) { 44 | if ( isset( $this->container[ $class ] ) ) { 45 | return $this->container[ $class ]; 46 | } 47 | 48 | if ( ! class_exists( $class ) ) { 49 | throw new ClassNotFoundException( 'Class not found: ' . $class ); 50 | } 51 | 52 | return new $class(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Application/HasAliasesTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Closure; 13 | use BadMethodCallException; 14 | use WPEmerge\Support\Arr; 15 | 16 | /** 17 | * Add methods to classes at runtime. 18 | * Loosely based on spatie/macroable. 19 | * 20 | * @codeCoverageIgnore 21 | */ 22 | trait HasAliasesTrait { 23 | /** 24 | * Registered aliases. 25 | * 26 | * @var array 27 | */ 28 | protected $aliases = []; 29 | 30 | /** 31 | * Get whether an alias is registered. 32 | * 33 | * @param string $alias 34 | * @return boolean 35 | */ 36 | public function hasAlias( $alias ) { 37 | return isset( $this->aliases[ $alias ] ); 38 | } 39 | 40 | /** 41 | * Get a registered alias. 42 | * 43 | * @param string $alias 44 | * @return array|null 45 | */ 46 | public function getAlias( $alias ) { 47 | if ( ! $this->hasAlias( $alias ) ) { 48 | return null; 49 | } 50 | 51 | return $this->aliases[ $alias ]; 52 | } 53 | 54 | /** 55 | * Register an alias. 56 | * Useful when passed the return value of getAlias() to restore 57 | * a previously registered alias, for example. 58 | * 59 | * @param array $alias 60 | * @return void 61 | */ 62 | public function setAlias( $alias ) { 63 | $name = Arr::get( $alias, 'name' ); 64 | 65 | $this->aliases[ $name ] = [ 66 | 'name' => $name, 67 | 'target' => Arr::get( $alias, 'target' ), 68 | 'method' => Arr::get( $alias, 'method', '' ), 69 | ]; 70 | } 71 | 72 | /** 73 | * Register an alias. 74 | * Identical to setAlias but with a more user-friendly interface. 75 | * 76 | * @codeCoverageIgnore 77 | * @param string $alias 78 | * @param string|Closure $target 79 | * @param string $method 80 | * @return void 81 | */ 82 | public function alias( $alias, $target, $method = '' ) { 83 | $this->setAlias( [ 84 | 'name' => $alias, 85 | 'target' => $target, 86 | 'method' => $method, 87 | ] ); 88 | } 89 | 90 | /** 91 | * Call alias if registered. 92 | * 93 | * @param string $method 94 | * @param array $parameters 95 | * @return mixed 96 | */ 97 | public function __call( $method, $parameters ) { 98 | if ( ! $this->hasAlias( $method ) ) { 99 | throw new BadMethodCallException( "Method {$method} does not exist." ); 100 | } 101 | 102 | $alias = $this->aliases[ $method ]; 103 | 104 | if ( $alias['target'] instanceof Closure ) { 105 | return call_user_func_array( $alias['target']->bindTo( $this, static::class ), $parameters ); 106 | } 107 | 108 | $target = $this->resolve( $alias['target'] ); 109 | 110 | if ( ! empty( $alias['method'] ) ) { 111 | return call_user_func_array( [$target, $alias['method']], $parameters ); 112 | } 113 | 114 | return $target; 115 | } 116 | 117 | /** 118 | * Resolve a dependency from the IoC container. 119 | * 120 | * @param string $key 121 | * @return mixed|null 122 | */ 123 | abstract public function resolve( $key ); 124 | } 125 | -------------------------------------------------------------------------------- /src/Application/HasContainerTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Pimple\Container; 13 | 14 | /** 15 | * Holds an IoC container. 16 | */ 17 | trait HasContainerTrait { 18 | /** 19 | * IoC container. 20 | * 21 | * @var Container 22 | */ 23 | protected $container = null; 24 | 25 | /** 26 | * Get the IoC container instance. 27 | * 28 | * @codeCoverageIgnore 29 | * @return Container 30 | */ 31 | public function container() { 32 | return $this->container; 33 | } 34 | 35 | /** 36 | * Set the IoC container instance. 37 | * 38 | * @codeCoverageIgnore 39 | * @param Container $container 40 | * @return void 41 | */ 42 | public function setContainer( $container ) { 43 | $this->container = $container; 44 | } 45 | 46 | /** 47 | * Resolve a dependency from the IoC container. 48 | * 49 | * @param string $key 50 | * @return mixed|null 51 | */ 52 | public function resolve( $key ) { 53 | if ( ! isset( $this->container()[ $key ] ) ) { 54 | return null; 55 | } 56 | 57 | return $this->container()[ $key ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Application/LoadsServiceProvidersTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Application; 11 | 12 | use Pimple\Container; 13 | use WPEmerge\Controllers\ControllersServiceProvider; 14 | use WPEmerge\Csrf\CsrfServiceProvider; 15 | use WPEmerge\Exceptions\ConfigurationException; 16 | use WPEmerge\Exceptions\ExceptionsServiceProvider; 17 | use WPEmerge\Flash\FlashServiceProvider; 18 | use WPEmerge\Input\OldInputServiceProvider; 19 | use WPEmerge\Kernels\KernelsServiceProvider; 20 | use WPEmerge\Middleware\MiddlewareServiceProvider; 21 | use WPEmerge\Requests\RequestsServiceProvider; 22 | use WPEmerge\Responses\ResponsesServiceProvider; 23 | use WPEmerge\Routing\RoutingServiceProvider; 24 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 25 | use WPEmerge\Support\Arr; 26 | use WPEmerge\View\ViewServiceProvider; 27 | 28 | /** 29 | * Load service providers. 30 | */ 31 | trait LoadsServiceProvidersTrait { 32 | /** 33 | * Array of default service providers. 34 | * 35 | * @var string[] 36 | */ 37 | protected $service_providers = [ 38 | ApplicationServiceProvider::class, 39 | KernelsServiceProvider::class, 40 | ExceptionsServiceProvider::class, 41 | RequestsServiceProvider::class, 42 | ResponsesServiceProvider::class, 43 | RoutingServiceProvider::class, 44 | ViewServiceProvider::class, 45 | ControllersServiceProvider::class, 46 | MiddlewareServiceProvider::class, 47 | CsrfServiceProvider::class, 48 | FlashServiceProvider::class, 49 | OldInputServiceProvider::class, 50 | ]; 51 | 52 | /** 53 | * Register and bootstrap all service providers. 54 | * 55 | * @codeCoverageIgnore 56 | * @param Container $container 57 | * @return void 58 | */ 59 | protected function loadServiceProviders( Container $container ) { 60 | $container[ WPEMERGE_SERVICE_PROVIDERS_KEY ] = array_merge( 61 | $this->service_providers, 62 | Arr::get( $container[ WPEMERGE_CONFIG_KEY ], 'providers', [] ) 63 | ); 64 | 65 | $service_providers = array_map( function ( $service_provider ) use ( $container ) { 66 | if ( ! is_subclass_of( $service_provider, ServiceProviderInterface::class ) ) { 67 | throw new ConfigurationException( 68 | 'The following class is not defined or does not implement ' . 69 | ServiceProviderInterface::class . ': ' . $service_provider 70 | ); 71 | } 72 | 73 | // Provide container access to the service provider instance 74 | // so bootstrap hooks can be unhooked e.g.: 75 | // remove_action( 'some_action', [\App::resolve( SomeServiceProvider::class ), 'methodAddedToAction'] ); 76 | $container[ $service_provider ] = new $service_provider(); 77 | 78 | return $container[ $service_provider ]; 79 | }, $container[ WPEMERGE_SERVICE_PROVIDERS_KEY ] ); 80 | 81 | $this->registerServiceProviders( $service_providers, $container ); 82 | $this->bootstrapServiceProviders( $service_providers, $container ); 83 | } 84 | 85 | /** 86 | * Register all service providers. 87 | * 88 | * @param ServiceProviderInterface[] $service_providers 89 | * @param Container $container 90 | * @return void 91 | */ 92 | protected function registerServiceProviders( $service_providers, Container $container ) { 93 | foreach ( $service_providers as $provider ) { 94 | $provider->register( $container ); 95 | } 96 | } 97 | 98 | /** 99 | * Bootstrap all service providers. 100 | * 101 | * @param ServiceProviderInterface[] $service_providers 102 | * @param Container $container 103 | * @return void 104 | */ 105 | protected function bootstrapServiceProviders( $service_providers, Container $container ) { 106 | foreach ( $service_providers as $provider ) { 107 | $provider->bootstrap( $container ); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Controllers/ControllersServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Controllers; 11 | 12 | use WPEmerge; 13 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 14 | 15 | /** 16 | * Provide controller dependencies 17 | * 18 | * @codeCoverageIgnore 19 | */ 20 | class ControllersServiceProvider implements ServiceProviderInterface { 21 | /** 22 | * {@inheritDoc} 23 | */ 24 | public function register( $container ) { 25 | $container[ WordPressController::class ] = function ( $c ) { 26 | return new WordPressController( $c[ WPEMERGE_VIEW_SERVICE_KEY ] ); 27 | }; 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | public function bootstrap( $container ) { 34 | // Nothing to bootstrap. 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Controllers/WordPressController.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright 2017-2019 Atanas Angelov 5 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 6 | * @link https://wpemerge.com/ 7 | */ /** @noinspection PhpUnusedParameterInspection */ 8 | namespace WPEmerge\Controllers; 9 | 10 | use Psr\Http\Message\ResponseInterface; 11 | use WPEmerge\Exceptions\ConfigurationException; 12 | use WPEmerge\Requests\RequestInterface; 13 | use WPEmerge\View\ViewService; 14 | 15 | /** 16 | * Handles normal WordPress requests without interfering 17 | * Useful if you only want to add a middleware to a route without handling the output 18 | * 19 | * @codeCoverageIgnore 20 | */ 21 | class WordPressController { 22 | /** 23 | * View service. 24 | * 25 | * @var ViewService 26 | */ 27 | protected $view_service = null; 28 | 29 | /** 30 | * Constructor. 31 | * 32 | * @codeCoverageIgnore 33 | * @param ViewService $view_service 34 | */ 35 | public function __construct( ViewService $view_service ) { 36 | $this->view_service = $view_service; 37 | } 38 | 39 | /** 40 | * Default WordPress handler. 41 | * 42 | * @param RequestInterface $request 43 | * @param string $view 44 | * @return ResponseInterface 45 | */ 46 | public function handle( RequestInterface $request, $view = '' ) { 47 | if ( is_admin() || wp_doing_ajax() ) { 48 | throw new ConfigurationException( 49 | 'Attempted to run the default WordPress controller on an ' . 50 | 'admin or AJAX page. Did you miss to specify a custom handler for ' . 51 | 'a route or accidentally used \App::route()->all() during admin ' . 52 | 'requests?' 53 | ); 54 | } 55 | 56 | if ( empty( $view ) ) { 57 | throw new ConfigurationException( 58 | 'No view loaded for default WordPress controller. ' . 59 | 'Did you miss to specify a custom handler for an ajax or admin route?' 60 | ); 61 | } 62 | 63 | $status_code = http_response_code(); 64 | 65 | return $this->view_service->make( $view ) 66 | ->toResponse() 67 | ->withStatus( is_int( $status_code ) ? $status_code : 200 ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Csrf/Csrf.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Csrf; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Provide CSRF protection utilities through WordPress nonces. 16 | */ 17 | class Csrf { 18 | /** 19 | * Convenience header to check for the token. 20 | * 21 | * @var string 22 | */ 23 | protected $header = 'X-CSRF-TOKEN'; 24 | 25 | /** 26 | * GET/POST parameter key to check for the token. 27 | * 28 | * @var string 29 | */ 30 | protected $key = ''; 31 | 32 | /** 33 | * Maximum token lifetime. 34 | * 35 | * @link https://codex.wordpress.org/Function_Reference/wp_verify_nonce 36 | * @var integer 37 | */ 38 | protected $maximum_lifetime = 2; 39 | 40 | /** 41 | * Last generated tokens. 42 | * 43 | * @var array 44 | */ 45 | protected $tokens = []; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @codeCoverageIgnore 51 | * @param string $key 52 | * @param integer $maximum_lifetime 53 | */ 54 | public function __construct( $key = '__wpemergeCsrfToken', $maximum_lifetime = 2 ) { 55 | $this->key = $key; 56 | $this->maximum_lifetime = $maximum_lifetime; 57 | } 58 | 59 | /** 60 | * Get the last generated token. 61 | * 62 | * @param int|string $action 63 | * @return string 64 | */ 65 | public function getToken( $action = -1 ) { 66 | if ( ! isset( $this->tokens[ $action ] ) ) { 67 | return $this->generateToken( $action ); 68 | } 69 | 70 | return $this->tokens[ $action ]; 71 | } 72 | 73 | /** 74 | * Get the csrf token from a request. 75 | * 76 | * @param RequestInterface $request 77 | * @return string 78 | */ 79 | public function getTokenFromRequest( RequestInterface $request ) { 80 | $query = $request->query( $this->key ); 81 | if ( $query ) { 82 | return $query; 83 | } 84 | 85 | $body = $request->body( $this->key ); 86 | if ( $body ) { 87 | return $body; 88 | } 89 | 90 | $header = $request->getHeaderLine( $this->header ); 91 | if ( $header ) { 92 | return $header; 93 | } 94 | 95 | return ''; 96 | } 97 | 98 | /** 99 | * Generate a new token. 100 | * 101 | * @param int|string $action 102 | * @return string 103 | */ 104 | public function generateToken( $action = -1 ) { 105 | $action = $action === -1 ? session_id() : $action; 106 | 107 | $this->tokens[ $action ] = wp_create_nonce( $action ); 108 | 109 | return $this->getToken( $action ); 110 | } 111 | 112 | /** 113 | * Check if a token is valid. 114 | * 115 | * @param string $token 116 | * @param int|string $action 117 | * @return boolean 118 | */ 119 | public function isValidToken( $token, $action = -1 ) { 120 | $action = $action === -1 ? session_id() : $action; 121 | $lifetime = (int) wp_verify_nonce( $token, $action ); 122 | 123 | return ( $lifetime > 0 && $lifetime <= $this->maximum_lifetime ); 124 | } 125 | 126 | /** 127 | * Add the token to a URL. 128 | * 129 | * @param string $url 130 | * @param int|string $action 131 | * @return string 132 | */ 133 | public function url( $url, $action = -1 ) { 134 | return add_query_arg( $this->key, $this->getToken( $action ), $url ); 135 | } 136 | 137 | /** 138 | * Return the markup for a hidden input which holds the current token. 139 | * 140 | * @param int|string $action 141 | * @return void 142 | */ 143 | public function field( $action = -1 ) { 144 | echo sprintf( 145 | '', 146 | esc_attr( $this->key ), 147 | esc_attr( $this->getToken( $action ) ) 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Csrf/CsrfMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Csrf; 11 | 12 | use Closure; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Requests\RequestInterface; 15 | 16 | /** 17 | * Store current request data and clear old request data 18 | */ 19 | class CsrfMiddleware { 20 | /** 21 | * CSRF service. 22 | * 23 | * @var Csrf 24 | */ 25 | protected $csrf = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param Csrf $csrf 31 | */ 32 | public function __construct( $csrf ) { 33 | $this->csrf = $csrf; 34 | } 35 | 36 | /** 37 | * Reject requests that fail nonce validation. 38 | * 39 | * @param RequestInterface $request 40 | * @param Closure $next 41 | * @param mixed $action 42 | * @return ResponseInterface 43 | * @throws InvalidCsrfTokenException 44 | */ 45 | public function handle( RequestInterface $request, Closure $next, $action = -1 ) { 46 | if ( ! $request->isReadVerb() ) { 47 | $token = $this->csrf->getTokenFromRequest( $request ); 48 | if ( ! $this->csrf->isValidToken( $token, $action ) ) { 49 | throw new InvalidCsrfTokenException(); 50 | } 51 | } 52 | 53 | $this->csrf->generateToken( $action ); 54 | 55 | return $next( $request ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Csrf/CsrfServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Csrf; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide CSRF dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class CsrfServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ WPEMERGE_CSRF_KEY ] = function () { 25 | return new Csrf(); 26 | }; 27 | 28 | $container[ CsrfMiddleware::class ] = function ( $c ) { 29 | return new CsrfMiddleware( $c[ WPEMERGE_CSRF_KEY ] ); 30 | }; 31 | 32 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 33 | $app->alias( 'csrf', WPEMERGE_CSRF_KEY ); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function bootstrap( $container ) { 40 | // Nothing to bootstrap. 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Csrf/InvalidCsrfTokenException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Csrf; 11 | 12 | use WPEmerge\Exceptions\Exception; 13 | 14 | class InvalidCsrfTokenException extends Exception { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/ClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | class ClassNotFoundException extends ConfigurationException { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | class ConfigurationException extends Exception { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | use Exception as PhpException; 13 | use Psr\Http\Message\ResponseInterface; 14 | use Whoops\RunInterface; 15 | use WPEmerge\Csrf\InvalidCsrfTokenException; 16 | use WPEmerge\Requests\RequestInterface; 17 | use WPEmerge\Responses\ResponseService; 18 | use WPEmerge\Routing\NotFoundException; 19 | use WPEmerge\Support\Arr; 20 | 21 | class ErrorHandler implements ErrorHandlerInterface { 22 | /** 23 | * Response service. 24 | * 25 | * @var ResponseService 26 | */ 27 | protected $response_service = null; 28 | 29 | /** 30 | * Pretty handler. 31 | * 32 | * @var RunInterface|null 33 | */ 34 | protected $whoops = null; 35 | 36 | /** 37 | * Whether debug mode is enabled. 38 | * 39 | * @var boolean 40 | */ 41 | protected $debug = false; 42 | 43 | /** 44 | * Constructor. 45 | * 46 | * @codeCoverageIgnore 47 | * @param ResponseService $response_service 48 | * @param RunInterface|null $whoops 49 | * @param boolean $debug 50 | */ 51 | public function __construct( $response_service, $whoops, $debug = false ) { 52 | $this->response_service = $response_service; 53 | $this->whoops = $whoops; 54 | $this->debug = $debug; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | * @codeCoverageIgnore 60 | */ 61 | public function register() { 62 | if ( $this->debug && $this->whoops !== null ) { 63 | $this->whoops->register(); 64 | } 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | * @codeCoverageIgnore 70 | */ 71 | public function unregister() { 72 | if ( $this->debug && $this->whoops !== null ) { 73 | $this->whoops->unregister(); 74 | } 75 | } 76 | 77 | /** 78 | * Convert an exception to a ResponseInterface instance if possible. 79 | * 80 | * @param PhpException $exception 81 | * @return ResponseInterface|false 82 | */ 83 | protected function toResponse( $exception ) { 84 | // @codeCoverageIgnoreStart 85 | if ( $exception instanceof InvalidCsrfTokenException ) { 86 | wp_nonce_ays( '' ); 87 | } 88 | // @codeCoverageIgnoreEnd 89 | 90 | if ( $exception instanceof NotFoundException ) { 91 | return $this->response_service->error( 404 ); 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /** 98 | * Convert an exception to a debug ResponseInterface instance if possible. 99 | * 100 | * @throws PhpException 101 | * @param RequestInterface $request 102 | * @param PhpException $exception 103 | * @return ResponseInterface 104 | */ 105 | protected function toDebugResponse( RequestInterface $request, PhpException $exception ) { 106 | if ( $request->isAjax() ) { 107 | return $this->response_service->json( [ 108 | 'message' => $exception->getMessage(), 109 | 'exception' => get_class( $exception ), 110 | 'file' => $exception->getFile(), 111 | 'line' => $exception->getLine(), 112 | 'trace' => array_map( function ( $trace ) { 113 | return Arr::except( $trace, ['args'] ); 114 | }, $exception->getTrace() ), 115 | ] )->withStatus( 500 ); 116 | } 117 | 118 | if ( $this->whoops !== null ) { 119 | return $this->toPrettyErrorResponse( $exception ); 120 | } 121 | 122 | throw $exception; 123 | } 124 | 125 | /** 126 | * Convert an exception to a pretty error response. 127 | * 128 | * @codeCoverageIgnore 129 | * @param PhpException $exception 130 | * @return ResponseInterface 131 | */ 132 | protected function toPrettyErrorResponse( $exception ) { 133 | $method = RunInterface::EXCEPTION_HANDLER; 134 | ob_start(); 135 | $this->whoops->$method( $exception ); 136 | $response = ob_get_clean(); 137 | return $this->response_service->output( $response )->withStatus( 500 ); 138 | } 139 | 140 | /** 141 | * {@inheritDoc} 142 | * @throws PhpException 143 | */ 144 | public function getResponse( RequestInterface $request, PhpException $exception ) { 145 | $response = $this->toResponse( $exception ); 146 | 147 | if ( $response !== false ) { 148 | return $response; 149 | } 150 | 151 | // @codeCoverageIgnoreStart 152 | if ( ! defined( 'WPEMERGE_TEST_DIR' ) ) { 153 | // Only log errors if we are not running the WP Emerge test suite. 154 | error_log( $exception ); 155 | } 156 | // @codeCoverageIgnoreEnd 157 | 158 | if ( ! $this->debug ) { 159 | return $this->response_service->error( 500 ); 160 | } 161 | 162 | return $this->toDebugResponse( $request, $exception ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Exceptions/ErrorHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | use Exception as PhpException; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Requests\RequestInterface; 15 | 16 | interface ErrorHandlerInterface { 17 | /** 18 | * Register any necessary error, exception and shutdown handlers. 19 | * 20 | * @return void 21 | */ 22 | public function register(); 23 | 24 | /** 25 | * Unregister any registered error, exception and shutdown handlers. 26 | * 27 | * @return void 28 | */ 29 | public function unregister(); 30 | 31 | /** 32 | * Get a response representing the specified exception. 33 | * 34 | * @param RequestInterface $request 35 | * @param PhpException $exception 36 | * @return ResponseInterface 37 | */ 38 | public function getResponse( RequestInterface $request, PhpException $exception ); 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | use Exception as PhpException; 13 | 14 | class Exception extends PhpException { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/ExceptionsServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Exceptions; 11 | 12 | use Pimple\Container; 13 | use Whoops\Handler\PrettyPageHandler; 14 | use Whoops\Run; 15 | use WPEmerge\Exceptions\Whoops\DebugDataProvider; 16 | use WPEmerge\ServiceProviders\ExtendsConfigTrait; 17 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 18 | 19 | /** 20 | * Provide exceptions dependencies. 21 | * 22 | * @codeCoverageIgnore 23 | */ 24 | class ExceptionsServiceProvider implements ServiceProviderInterface { 25 | use ExtendsConfigTrait; 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function register( $container ) { 31 | $debug = defined( 'WP_DEBUG' ) && WP_DEBUG; 32 | 33 | $this->extendConfig( $container, 'debug', [ 34 | 'enable' => $debug, 35 | 'pretty_errors' => $debug, 36 | ] ); 37 | 38 | $this->registerPrettyErrorHandler( $container ); 39 | $this->registerErrorHandler( $container ); 40 | } 41 | 42 | /** 43 | * Register the pretty error handler service. 44 | * 45 | * @param Container $container 46 | */ 47 | protected function registerPrettyErrorHandler( $container ) { 48 | $container[ DebugDataProvider::class ] = function ( $container ) { 49 | return new DebugDataProvider( $container ); 50 | }; 51 | 52 | $container[ PrettyPageHandler::class ] = function ( $container ) { 53 | $handler = new PrettyPageHandler(); 54 | $handler->addResourcePath( implode( DIRECTORY_SEPARATOR, [WPEMERGE_DIR, 'src', 'Exceptions', 'Whoops'] ) ); 55 | 56 | $handler->addDataTableCallback( 'WP Emerge: Route', function ( $inspector ) use ( $container ) { 57 | return $container[ DebugDataProvider::class ]->route( $inspector ); 58 | } ); 59 | 60 | return $handler; 61 | }; 62 | 63 | $container[ Run::class ] = function ( $container ) { 64 | if ( ! class_exists( Run::class ) ) { 65 | return null; 66 | } 67 | 68 | $run = new Run(); 69 | $run->allowQuit( false ); 70 | 71 | $handler = $container[ PrettyPageHandler::class ]; 72 | 73 | if ( $handler ) { 74 | $run->pushHandler( $handler ); 75 | } 76 | 77 | return $run; 78 | }; 79 | } 80 | 81 | /** 82 | * Register the error handler service. 83 | * 84 | * @param Container $container 85 | */ 86 | protected function registerErrorHandler( $container ) { 87 | $container[ WPEMERGE_EXCEPTIONS_ERROR_HANDLER_KEY ] = function ( $container ) { 88 | $debug = $container[ WPEMERGE_CONFIG_KEY ]['debug']; 89 | $whoops = $debug['pretty_errors'] ? $container[ Run::class ] : null; 90 | return new ErrorHandler( $container[ WPEMERGE_RESPONSE_SERVICE_KEY ], $whoops, $debug['enable'] ); 91 | }; 92 | 93 | $container[ WPEMERGE_EXCEPTIONS_CONFIGURATION_ERROR_HANDLER_KEY ] = function ( $container ) { 94 | $debug = $container[ WPEMERGE_CONFIG_KEY ]['debug']; 95 | $whoops = $debug['pretty_errors'] ? $container[ Run::class ] : null; 96 | return new ErrorHandler( $container[ WPEMERGE_RESPONSE_SERVICE_KEY ], $whoops, $debug['enable'] ); 97 | }; 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public function bootstrap( $container ) { 104 | // Nothing to bootstrap. 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Exceptions/Whoops/DebugDataProvider.php: -------------------------------------------------------------------------------- 1 | container = $container; 27 | } 28 | 29 | /** 30 | * Convert a value to a scalar representation. 31 | * 32 | * @param mixed $value 33 | * @return mixed 34 | */ 35 | public function toScalar( $value ) { 36 | $type = gettype( $value ); 37 | 38 | if ( ! is_scalar( $value ) ) { 39 | $value = '(' . $type . ')' . ( $type === 'object' ? ' ' . get_class( $value ) : '' ); 40 | } 41 | 42 | return $value; 43 | } 44 | 45 | /** 46 | * Return printable data about the current route. 47 | * 48 | * @param \Whoops\Exception\Inspector $inspector 49 | * @return array 50 | */ 51 | public function route( $inspector ) { 52 | /** @var \WPEmerge\Routing\RouteInterface|null $route */ 53 | $route = $this->container[ WPEMERGE_ROUTING_ROUTER_KEY ]->getCurrentRoute(); 54 | 55 | if ( ! $route ) { 56 | return []; 57 | } 58 | 59 | $attributes = []; 60 | 61 | foreach ( $route->getAttributes() as $attribute => $value ) { 62 | // Only convert the first level of an array to scalar for simplicity. 63 | if ( is_array( $value ) ) { 64 | $value = '[' . implode( ', ', array_map( [$this, 'toScalar'], $value ) ) . ']'; 65 | } else { 66 | $value = $this->toScalar( $value ); 67 | } 68 | 69 | $attributes[ $attribute ] = $value; 70 | } 71 | 72 | return $attributes; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Exceptions/Whoops/views/layout.html.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | /** 11 | * Layout template file for Whoops's pretty error output. 12 | * 13 | * @noinspection ALL 14 | */ 15 | 16 | $is_admin = function_exists( 'is_admin' ) && is_admin() && apply_filters( 'wpemerge.pretty_errors.apply_admin_styles', true ); 17 | $is_ajax = function_exists( 'wp_doing_ajax' ) && wp_doing_ajax(); 18 | 19 | if ( $is_admin && ! $is_ajax ) { 20 | ?> 21 | 22 | 55 | 56 | 65 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ' . $tpl->escape( $page_title ) . '' ?> 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/Exceptions/Whoops/views/wpemerge-body.html.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | /** 11 | * @noinspection ALL 12 | */ 13 | ?> 14 |
15 | 16 | 17 |
18 |
19 | 20 | render( $panel_left_outer ) ?> 21 | 22 | render( $panel_details_outer ) ?> 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/Flash/Flash.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Flash; 11 | 12 | use ArrayAccess; 13 | use WPEmerge\Exceptions\ConfigurationException; 14 | use WPEmerge\Helpers\MixedType; 15 | use WPEmerge\Support\Arr; 16 | 17 | /** 18 | * Provide a way to flash data into the session for the next request. 19 | */ 20 | class Flash { 21 | /** 22 | * Keys for different request contexts. 23 | */ 24 | const CURRENT_KEY = 'current'; 25 | const NEXT_KEY = 'next'; 26 | 27 | /** 28 | * Key to store flashed data in store with. 29 | * 30 | * @var string 31 | */ 32 | protected $store_key = ''; 33 | 34 | /** 35 | * Root store array or object implementing ArrayAccess. 36 | * 37 | * @var array|ArrayAccess 38 | */ 39 | protected $store = null; 40 | 41 | /** 42 | * Flash store array. 43 | * 44 | * @var array 45 | */ 46 | protected $flashed = []; 47 | 48 | /** 49 | * Constructor. 50 | * 51 | * @codeCoverageIgnore 52 | * @param array|ArrayAccess $store 53 | * @param string $store_key 54 | */ 55 | public function __construct( &$store, $store_key = '__wpemergeFlash' ) { 56 | $this->store_key = $store_key; 57 | $this->setStore( $store ); 58 | } 59 | 60 | /** 61 | * Get whether a store object is valid. 62 | * 63 | * @param mixed $store 64 | * @return boolean 65 | */ 66 | protected function isValidStore( $store ) { 67 | return ( is_array( $store ) || $store instanceof ArrayAccess ); 68 | } 69 | 70 | /** 71 | * Throw an exception if store is not valid. 72 | * 73 | * @return void 74 | */ 75 | protected function validateStore() { 76 | if ( ! $this->isValidStore( $this->store ) ) { 77 | throw new ConfigurationException( 78 | 'Attempted to use Flash without an active session. ' . 79 | 'Did you miss to call session_start()?' 80 | ); 81 | } 82 | } 83 | 84 | /** 85 | * Get the store for flash messages. 86 | * 87 | * @return array|ArrayAccess 88 | */ 89 | public function getStore() { 90 | return $this->store; 91 | } 92 | 93 | /** 94 | * Set the store for flash messages. 95 | * 96 | * @param array|ArrayAccess $store 97 | * @return void 98 | */ 99 | public function setStore( &$store ) { 100 | if ( ! $this->isValidStore( $store ) ) { 101 | return; 102 | } 103 | 104 | $this->store = &$store; 105 | 106 | if ( ! isset( $this->store[ $this->store_key ] ) ) { 107 | $this->store[ $this->store_key ] = [ 108 | static::CURRENT_KEY => [], 109 | static::NEXT_KEY => [], 110 | ]; 111 | } 112 | 113 | $this->flashed = $store[ $this->store_key ]; 114 | } 115 | 116 | /** 117 | * Get whether the flash service is enabled. 118 | * 119 | * @return boolean 120 | */ 121 | public function enabled() { 122 | return $this->isValidStore( $this->store ); 123 | } 124 | 125 | /** 126 | * Get the entire store or the values for a key for a request. 127 | * 128 | * @param string $request_key 129 | * @param string|null $key 130 | * @param mixed $default 131 | * @return mixed 132 | */ 133 | protected function getFromRequest( $request_key, $key = null, $default = [] ) { 134 | $this->validateStore(); 135 | 136 | if ( $key === null ) { 137 | return Arr::get( $this->flashed, $request_key, $default ); 138 | } 139 | 140 | return Arr::get( $this->flashed[ $request_key ], $key, $default ); 141 | } 142 | 143 | /** 144 | * Add values for a key for a request. 145 | * 146 | * @param string $request_key 147 | * @param string $key 148 | * @param mixed $new_items 149 | * @return void 150 | */ 151 | protected function addToRequest( $request_key, $key, $new_items ) { 152 | $this->validateStore(); 153 | 154 | $new_items = MixedType::toArray( $new_items ); 155 | $items = MixedType::toArray( $this->getFromRequest( $request_key, $key, [] ) ); 156 | $this->flashed[ $request_key ][ $key ] = array_merge( $items, $new_items ); 157 | } 158 | 159 | /** 160 | * Remove all values or values for a key from a request. 161 | * 162 | * @param string $request_key 163 | * @param string|null $key 164 | * @return void 165 | */ 166 | protected function clearFromRequest( $request_key, $key = null ) { 167 | $this->validateStore(); 168 | 169 | $keys = $key === null ? array_keys( $this->flashed[ $request_key ] ) : [$key]; 170 | foreach ( $keys as $k ) { 171 | unset( $this->flashed[ $request_key ][ $k ] ); 172 | } 173 | } 174 | 175 | /** 176 | * Add values for a key for the next request. 177 | * 178 | * @param string $key 179 | * @param mixed $new_items 180 | * @return void 181 | */ 182 | public function add( $key, $new_items ) { 183 | $this->addToRequest( static::NEXT_KEY, $key, $new_items ); 184 | } 185 | 186 | /** 187 | * Add values for a key for the current request. 188 | * 189 | * @param string $key 190 | * @param mixed $new_items 191 | * @return void 192 | */ 193 | public function addNow( $key, $new_items ) { 194 | $this->addToRequest( static::CURRENT_KEY, $key, $new_items ); 195 | } 196 | 197 | /** 198 | * Get the entire store or the values for a key for the current request. 199 | * 200 | * @param string|null $key 201 | * @param mixed $default 202 | * @return mixed 203 | */ 204 | public function get( $key = null, $default = [] ) { 205 | return $this->getFromRequest( static::CURRENT_KEY, $key, $default ); 206 | } 207 | 208 | /** 209 | * Get the entire store or the values for a key for the next request. 210 | * 211 | * @param string|null $key 212 | * @param mixed $default 213 | * @return mixed 214 | */ 215 | public function getNext( $key = null, $default = [] ) { 216 | return $this->getFromRequest( static::NEXT_KEY, $key, $default ); 217 | } 218 | 219 | /** 220 | * Clear the entire store or the values for a key for the current request. 221 | * 222 | * @param string|null $key 223 | * @return void 224 | */ 225 | public function clear( $key = null ) { 226 | $this->clearFromRequest( static::CURRENT_KEY, $key ); 227 | } 228 | 229 | /** 230 | * Clear the entire store or the values for a key for the next request. 231 | * 232 | * @param string|null $key 233 | * @return void 234 | */ 235 | public function clearNext( $key = null ) { 236 | $this->clearFromRequest( static::NEXT_KEY, $key ); 237 | } 238 | 239 | /** 240 | * Shift current store and replace it with next store. 241 | * 242 | * @return void 243 | */ 244 | public function shift() { 245 | $this->validateStore(); 246 | 247 | $this->flashed[ static::CURRENT_KEY ] = $this->flashed[ static::NEXT_KEY ]; 248 | $this->flashed[ static::NEXT_KEY ] = []; 249 | } 250 | 251 | /** 252 | * Save flashed data to store. 253 | * 254 | * @return void 255 | */ 256 | public function save() { 257 | $this->validateStore(); 258 | 259 | $this->store[ $this->store_key ] = $this->flashed; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Flash/FlashMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Flash; 11 | 12 | use Closure; 13 | use WPEmerge\Requests\RequestInterface; 14 | 15 | /** 16 | * Store current request data and clear old request data 17 | */ 18 | class FlashMiddleware { 19 | /** 20 | * Flash service. 21 | * 22 | * @var Flash 23 | */ 24 | protected $flash = null; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @codeCoverageIgnore 30 | * @param Flash $flash 31 | */ 32 | public function __construct( Flash $flash ) { 33 | $this->flash = $flash; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function handle( RequestInterface $request, Closure $next ) { 40 | $response = $next( $request ); 41 | 42 | if ( $this->flash->enabled() ) { 43 | $this->flash->shift(); 44 | $this->flash->save(); 45 | } 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Flash/FlashServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Flash; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide flash dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class FlashServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ WPEMERGE_FLASH_KEY ] = function ( $c ) { 25 | $session = null; 26 | if ( isset( $c[ WPEMERGE_SESSION_KEY ] ) ) { 27 | $session = &$c[ WPEMERGE_SESSION_KEY ]; 28 | } else if ( isset( $_SESSION ) ) { 29 | $session = &$_SESSION; 30 | } 31 | return new Flash( $session ); 32 | }; 33 | 34 | $container[ FlashMiddleware::class ] = function ( $c ) { 35 | return new FlashMiddleware( $c[ WPEMERGE_FLASH_KEY ] ); 36 | }; 37 | 38 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 39 | $app->alias( 'flash', WPEMERGE_FLASH_KEY ); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function bootstrap( $container ) { 46 | // Nothing to bootstrap. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Helpers/Arguments.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | /** 13 | * A collection of tools dealing with urls 14 | */ 15 | class Arguments { 16 | /** 17 | * Get a closure which will flip preceding optional arguments around. 18 | * @example list( $argument1, $argument2 ) = Arguments::flip( $argument1, $argument2 ); 19 | * 20 | * @return array 21 | */ 22 | public static function flip() { 23 | $arguments = func_get_args(); 24 | $first_null = array_search( null, $arguments, true ); 25 | 26 | if ( $first_null === false ) { 27 | return $arguments; 28 | } 29 | 30 | // Support integer keys only. 31 | $first_null = (int) $first_null; 32 | 33 | $arguments = array_values( array_merge( 34 | array_slice( $arguments, $first_null ), 35 | array_slice( $arguments, 0, $first_null ) 36 | ) ); 37 | 38 | return $arguments; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Helpers/Handler.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | use Closure; 13 | use WPEmerge\Application\GenericFactory; 14 | use WPEmerge\Exceptions\ClassNotFoundException; 15 | use WPEmerge\Exceptions\ConfigurationException; 16 | use WPEmerge\Support\Arr; 17 | 18 | /** 19 | * Represent a generic handler - a Closure or a class method to be resolved from the service container 20 | */ 21 | class Handler { 22 | /** 23 | * Injection Factory. 24 | * 25 | * @var GenericFactory 26 | */ 27 | protected $factory = null; 28 | 29 | /** 30 | * Parsed handler 31 | * 32 | * @var array|Closure 33 | */ 34 | protected $handler = null; 35 | 36 | /** 37 | * Constructor 38 | * 39 | * @param GenericFactory $factory 40 | * @param string|array|Closure $raw_handler 41 | * @param string $default_method 42 | * @param string $namespace 43 | */ 44 | public function __construct( GenericFactory $factory, $raw_handler, $default_method = '', $namespace = '' ) { 45 | $this->factory = $factory; 46 | 47 | $handler = $this->parse( $raw_handler, $default_method, $namespace ); 48 | 49 | if ( $handler === null ) { 50 | throw new ConfigurationException( 'No or invalid handler provided.' ); 51 | } 52 | 53 | $this->handler = $handler; 54 | } 55 | 56 | /** 57 | * Parse a raw handler to a Closure or a [class, method, namespace] array 58 | * 59 | * @param string|array|Closure $raw_handler 60 | * @param string $default_method 61 | * @param string $namespace 62 | * @return array|Closure|null 63 | */ 64 | protected function parse( $raw_handler, $default_method, $namespace ) { 65 | if ( $raw_handler instanceof Closure ) { 66 | return $raw_handler; 67 | } 68 | 69 | if ( is_array( $raw_handler ) ) { 70 | return $this->parseFromArray( $raw_handler, $default_method, $namespace ); 71 | } 72 | 73 | return $this->parseFromString( $raw_handler, $default_method, $namespace ); 74 | } 75 | 76 | /** 77 | * Parse a [Class::class, 'method'] array handler to a [class, method, namespace] array 78 | * 79 | * @param array $raw_handler 80 | * @param string $default_method 81 | * @param string $namespace 82 | * @return array|null 83 | */ 84 | protected function parseFromArray( $raw_handler, $default_method, $namespace ) { 85 | $class = Arr::get( $raw_handler, 0, '' ); 86 | $class = preg_replace( '/^\\\\+/', '', $class ); 87 | $method = Arr::get( $raw_handler, 1, $default_method ); 88 | 89 | if ( empty( $class ) ) { 90 | return null; 91 | } 92 | 93 | if ( empty( $method ) ) { 94 | return null; 95 | } 96 | 97 | return [ 98 | 'class' => $class, 99 | 'method' => $method, 100 | 'namespace' => $namespace, 101 | ]; 102 | } 103 | 104 | /** 105 | * Parse a 'Controller@method' or 'Controller::method' string handler to a [class, method, namespace] array 106 | * 107 | * @param string $raw_handler 108 | * @param string $default_method 109 | * @param string $namespace 110 | * @return array|null 111 | */ 112 | protected function parseFromString( $raw_handler, $default_method, $namespace ) { 113 | return $this->parseFromArray( preg_split( '/@|::/', $raw_handler, 2 ), $default_method, $namespace ); 114 | } 115 | 116 | /** 117 | * Get the parsed handler 118 | * 119 | * @return array|Closure 120 | */ 121 | public function get() { 122 | return $this->handler; 123 | } 124 | 125 | /** 126 | * Make an instance of the handler. 127 | * 128 | * @return object 129 | */ 130 | public function make() { 131 | $handler = $this->get(); 132 | 133 | if ( $handler instanceof Closure ) { 134 | return $handler; 135 | } 136 | 137 | $namespace = $handler['namespace']; 138 | $class = $handler['class']; 139 | 140 | try { 141 | $instance = $this->factory->make( $class ); 142 | } catch ( ClassNotFoundException $e ) { 143 | try { 144 | $instance = $this->factory->make( $namespace . $class ); 145 | } catch ( ClassNotFoundException $e ) { 146 | throw new ClassNotFoundException( 'Class not found - tried: ' . $class . ', ' . $namespace . $class ); 147 | } 148 | } 149 | 150 | return $instance; 151 | } 152 | 153 | /** 154 | * Execute the parsed handler with any provided arguments and return the result. 155 | * 156 | * @param mixed ,...$arguments 157 | * @return mixed 158 | */ 159 | public function execute() { 160 | $arguments = func_get_args(); 161 | $instance = $this->make(); 162 | 163 | if ( $instance instanceof Closure ) { 164 | return call_user_func_array( $instance, $arguments ); 165 | } 166 | 167 | return call_user_func_array( [$instance, $this->get()['method']], $arguments ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Helpers/HandlerFactory.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | use Closure; 13 | use WPEmerge\Application\GenericFactory; 14 | 15 | /** 16 | * Handler factory. 17 | */ 18 | class HandlerFactory { 19 | /** 20 | * Injection Factory. 21 | * 22 | * @var GenericFactory 23 | */ 24 | protected $factory = null; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @codeCoverageIgnore 30 | * @param GenericFactory $factory 31 | */ 32 | public function __construct( GenericFactory $factory ) { 33 | $this->factory = $factory; 34 | } 35 | 36 | /** 37 | * Make a Handler. 38 | * 39 | * @codeCoverageIgnore 40 | * @param string|Closure $raw_handler 41 | * @param string $default_method 42 | * @param string $namespace 43 | * @return Handler 44 | */ 45 | public function make( $raw_handler, $default_method = '', $namespace = '' ) { 46 | return new Handler( $this->factory, $raw_handler, $default_method, $namespace ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Helpers/HasAttributesInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | /** 13 | * Represent an object which has an array of attributes. 14 | */ 15 | interface HasAttributesInterface { 16 | /** 17 | * Get attribute. 18 | * 19 | * @param string $attribute 20 | * @param mixed $default 21 | * @return mixed 22 | */ 23 | public function getAttribute( $attribute, $default = '' ); 24 | 25 | /** 26 | * Get all attributes. 27 | * 28 | * @return array 29 | */ 30 | public function getAttributes(); 31 | 32 | /** 33 | * Set attribute. 34 | * 35 | * @param string $attribute 36 | * @param mixed $value 37 | * @return void 38 | */ 39 | public function setAttribute( $attribute, $value ); 40 | 41 | /** 42 | * Fluent alias for setAttribute(). 43 | * 44 | * @param string $attribute 45 | * @param mixed $value 46 | * @return static $this 47 | */ 48 | public function attribute( $attribute, $value ); 49 | 50 | /** 51 | * Set attributes. 52 | * No attempt to merge attributes is done - this is a direct overwrite operation. 53 | * 54 | * @param array $attributes 55 | * @return void 56 | */ 57 | public function setAttributes( $attributes ); 58 | 59 | /** 60 | * Fluent alias for setAttributes(). 61 | * 62 | * @param array $attributes 63 | * @return static $this 64 | */ 65 | public function attributes( $attributes ); 66 | } 67 | -------------------------------------------------------------------------------- /src/Helpers/HasAttributesTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | use WPEmerge\Support\Arr; 13 | 14 | /** 15 | * Represent an object which has an array of attributes. 16 | */ 17 | trait HasAttributesTrait { 18 | /** 19 | * Attributes. 20 | * 21 | * @var array 22 | */ 23 | protected $attributes = []; 24 | 25 | /** 26 | * Get attribute. 27 | * 28 | * @param string $attribute 29 | * @param mixed $default 30 | * @return mixed 31 | */ 32 | public function getAttribute( $attribute, $default = '' ) { 33 | return Arr::get( $this->getAttributes(), $attribute, $default ); 34 | } 35 | 36 | /** 37 | * Get all attributes. 38 | * 39 | * @return array 40 | */ 41 | public function getAttributes() { 42 | return $this->attributes; 43 | } 44 | 45 | /** 46 | * Set attribute. 47 | * 48 | * @param string $attribute 49 | * @param mixed $value 50 | * @return void 51 | */ 52 | public function setAttribute( $attribute, $value ) { 53 | $this->setAttributes( array_merge( 54 | $this->getAttributes(), 55 | [$attribute => $value] 56 | ) ); 57 | } 58 | 59 | /** 60 | * Fluent alias for setAttribute(). 61 | * 62 | * @codeCoverageIgnore 63 | * @param string $attribute 64 | * @param mixed $value 65 | * @return static $this 66 | */ 67 | public function attribute( $attribute, $value ) { 68 | $this->setAttribute( $attribute, $value ); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Set all attributes. 75 | * No attempt to merge attributes is done - this is a direct overwrite operation. 76 | * 77 | * @param array $attributes 78 | * @return void 79 | */ 80 | public function setAttributes( $attributes ) { 81 | $this->attributes = $attributes; 82 | } 83 | 84 | /** 85 | * Fluent alias for setAttributes(). 86 | * 87 | * @codeCoverageIgnore 88 | * @param array $attributes 89 | * @return static $this 90 | */ 91 | public function attributes( $attributes ) { 92 | $this->setAttributes( $attributes ); 93 | 94 | return $this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Helpers/MixedType.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | class MixedType { 13 | /** 14 | * Converts a value to an array containing this value unless it is an array. 15 | * This will not convert objects like (array) casting does. 16 | * 17 | * @param mixed $argument 18 | * @return array 19 | */ 20 | public static function toArray( $argument ) { 21 | if ( ! is_array( $argument ) ) { 22 | $argument = [$argument]; 23 | } 24 | 25 | return $argument; 26 | } 27 | 28 | /** 29 | * Executes a value depending on what type it is and returns the result 30 | * Callable: call; return result 31 | * Instance: call method; return result 32 | * Class: instantiate; call method; return result 33 | * Other: return value without taking any action 34 | * 35 | * @noinspection PhpDocSignatureInspection 36 | * @param mixed $entity 37 | * @param array $arguments 38 | * @param string $method 39 | * @param callable $instantiator 40 | * @return mixed 41 | */ 42 | public static function value( 43 | $entity, 44 | $arguments = [], 45 | $method = '__invoke', 46 | $instantiator = 'static::instantiate' 47 | ) { 48 | if ( is_callable( $entity ) ) { 49 | return call_user_func_array( $entity, $arguments ); 50 | } 51 | 52 | if ( is_object( $entity ) ) { 53 | return call_user_func_array( [$entity, $method], $arguments ); 54 | } 55 | 56 | if ( static::isClass( $entity ) ) { 57 | return call_user_func_array( [call_user_func( $instantiator, $entity ), $method], $arguments ); 58 | } 59 | 60 | return $entity; 61 | } 62 | 63 | /** 64 | * Check if a value is a valid class name 65 | * 66 | * @param mixed $class_name 67 | * @return boolean 68 | */ 69 | public static function isClass( $class_name ) { 70 | return ( is_string( $class_name ) && class_exists( $class_name ) ); 71 | } 72 | 73 | /** 74 | * Create a new instance of the given class. 75 | * 76 | * @param string $class_name 77 | * @return object 78 | */ 79 | public static function instantiate( $class_name ) { 80 | return new $class_name(); 81 | } 82 | 83 | /** 84 | * Normalize a path's slashes according to the current OS. 85 | * Solves mixed slashes that are sometimes returned by WordPress core functions. 86 | * 87 | * @param string $path 88 | * @param string $slash 89 | * @return string 90 | */ 91 | public static function normalizePath( $path, $slash = DIRECTORY_SEPARATOR ) { 92 | return preg_replace( '~[' . preg_quote( '/\\', '~' ) . ']+~', $slash, $path ); 93 | } 94 | 95 | /** 96 | * Ensure path has a trailing slash. 97 | * 98 | * @param string $path 99 | * @param string $slash 100 | * @return string 101 | */ 102 | public static function addTrailingSlash( $path, $slash = DIRECTORY_SEPARATOR ) { 103 | $path = static::normalizePath( $path, $slash ); 104 | $path = preg_replace( '~' . preg_quote( $slash, '~' ) . '*$~', $slash, $path ); 105 | return $path; 106 | } 107 | 108 | /** 109 | * Ensure path does not have a trailing slash. 110 | * 111 | * @param string $path 112 | * @param string $slash 113 | * @return string 114 | */ 115 | public static function removeTrailingSlash( $path, $slash = DIRECTORY_SEPARATOR ) { 116 | $path = static::normalizePath( $path, $slash ); 117 | $path = preg_replace( '~' . preg_quote( $slash, '~' ) . '+$~', '', $path ); 118 | return $path; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Helpers/Url.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Helpers; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | use WPEmerge\Support\Arr; 14 | 15 | /** 16 | * A collection of tools dealing with URLs. 17 | */ 18 | class Url { 19 | /** 20 | * Get the path for the request relative to the home url. 21 | * Works only with absolute URLs. 22 | * 23 | * @param RequestInterface $request 24 | * @param string $home_url 25 | * @return string 26 | */ 27 | public static function getPath( RequestInterface $request, $home_url = '' ) { 28 | $parsed_request = wp_parse_url( $request->getUrl() ); 29 | $parsed_home = wp_parse_url( $home_url ? $home_url : home_url( '/' ) ); 30 | 31 | $request_path = Arr::get( $parsed_request, 'path', '/' ); 32 | $request_path = static::removeTrailingSlash( $request_path ); 33 | $request_path = static::addLeadingSlash( $request_path ); 34 | 35 | if ( $parsed_request['host'] !== $parsed_home['host'] ) { 36 | return $request_path; 37 | } 38 | 39 | $home_path = Arr::get( $parsed_home, 'path', '/' ); 40 | $home_path = static::removeTrailingSlash( $home_path ); 41 | $home_path = static::addLeadingSlash( $home_path ); 42 | $path = $request_path; 43 | 44 | if ( strpos( $request_path, $home_path ) === 0 ) { 45 | $path = substr( $request_path, strlen( $home_path ) ); 46 | } 47 | 48 | return static::addLeadingSlash( $path ); 49 | } 50 | 51 | /** 52 | * Ensure url has a leading slash 53 | * 54 | * @param string $url 55 | * @param boolean $leave_blank 56 | * @return string 57 | */ 58 | public static function addLeadingSlash( $url, $leave_blank = false ) { 59 | if ( $leave_blank && $url === '' ) { 60 | return ''; 61 | } 62 | 63 | return '/' . static::removeLeadingSlash( $url ); 64 | } 65 | 66 | /** 67 | * Ensure url does not have a leading slash 68 | * 69 | * @param string $url 70 | * @return string 71 | */ 72 | public static function removeLeadingSlash( $url ) { 73 | return preg_replace( '/^\/+/', '', $url ); 74 | } 75 | 76 | /** 77 | * Ensure url has a trailing slash 78 | * 79 | * @param string $url 80 | * @param boolean $leave_blank 81 | * @return string 82 | */ 83 | public static function addTrailingSlash( $url, $leave_blank = false ) { 84 | if ( $leave_blank && $url === '' ) { 85 | return ''; 86 | } 87 | 88 | return trailingslashit( $url ); 89 | } 90 | 91 | /** 92 | * Ensure url does not have a trailing slash 93 | * 94 | * @param string $url 95 | * @return string 96 | */ 97 | public static function removeTrailingSlash( $url ) { 98 | return untrailingslashit( $url ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Input/OldInput.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Input; 11 | 12 | use WPEmerge\Flash\Flash; 13 | use WPEmerge\Support\Arr; 14 | 15 | /** 16 | * Provide a way to get values from the previous request. 17 | */ 18 | class OldInput { 19 | /** 20 | * Flash service. 21 | * 22 | * @var Flash 23 | */ 24 | protected $flash = null; 25 | 26 | /** 27 | * Key to store the flashed data with. 28 | * 29 | * @var string 30 | */ 31 | protected $flash_key = ''; 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @codeCoverageIgnore 37 | * @param Flash $flash 38 | * @param string $flash_key 39 | */ 40 | public function __construct( Flash $flash, $flash_key = '__wpemergeOldInput' ) { 41 | $this->flash = $flash; 42 | $this->flash_key = $flash_key; 43 | } 44 | 45 | /** 46 | * Get whether the old input service is enabled. 47 | * 48 | * @return boolean 49 | */ 50 | public function enabled() { 51 | return $this->flash->enabled(); 52 | } 53 | 54 | /** 55 | * Get request value for key from the previous request. 56 | * 57 | * @param string $key 58 | * @param mixed $default 59 | * @return mixed 60 | */ 61 | public function get( $key, $default = null ) { 62 | return Arr::get( $this->flash->get( $this->flash_key, [] ), $key, $default ); 63 | } 64 | 65 | /** 66 | * Set input for the next request. 67 | * 68 | * @param array $input 69 | */ 70 | public function set( $input ) { 71 | $this->flash->add( $this->flash_key, $input ); 72 | } 73 | 74 | /** 75 | * Clear input for the next request. 76 | * 77 | * @return void 78 | */ 79 | public function clear() { 80 | $this->flash->clear( $this->flash_key ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Input/OldInputMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Input; 11 | 12 | use Closure; 13 | use WPEmerge\Requests\RequestInterface; 14 | 15 | /** 16 | * Store current request data and clear old request data 17 | */ 18 | class OldInputMiddleware { 19 | /** 20 | * OldInput service. 21 | * 22 | * @var OldInput 23 | */ 24 | protected $old_input = null; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @codeCoverageIgnore 30 | * @param OldInput $old_input 31 | */ 32 | public function __construct( OldInput $old_input ) { 33 | $this->old_input = $old_input; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function handle( RequestInterface $request, Closure $next ) { 40 | if ( $this->old_input->enabled() && $request->isPost() ) { 41 | $this->old_input->set( $request->body() ); 42 | } 43 | 44 | return $next( $request ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Input/OldInputServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Input; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide old input dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class OldInputServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ WPEMERGE_OLD_INPUT_KEY ] = function ( $c ) { 25 | return new OldInput( $c[ WPEMERGE_FLASH_KEY ] ); 26 | }; 27 | 28 | $container[ OldInputMiddleware::class ] = function ( $c ) { 29 | return new OldInputMiddleware( $c[ WPEMERGE_OLD_INPUT_KEY ] ); 30 | }; 31 | 32 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 33 | $app->alias( 'oldInput', WPEMERGE_OLD_INPUT_KEY ); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function bootstrap( $container ) { 40 | // Nothing to bootstrap. 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Kernels/HttpKernelInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Kernels; 11 | 12 | use Closure; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Helpers\Handler; 15 | use WPEmerge\Middleware\HasMiddlewareDefinitionsInterface; 16 | use WPEmerge\Requests\RequestInterface; 17 | 18 | /** 19 | * Describes how a request is handled. 20 | */ 21 | interface HttpKernelInterface extends HasMiddlewareDefinitionsInterface { 22 | /** 23 | * Bootstrap the kernel. 24 | * 25 | * @return void 26 | */ 27 | public function bootstrap(); 28 | 29 | /** 30 | * Run a response pipeline for the given request. 31 | * 32 | * @param RequestInterface $request 33 | * @param string[] $middleware 34 | * @param string|Closure|Handler $handler 35 | * @param array $arguments 36 | * @return ResponseInterface 37 | */ 38 | public function run( RequestInterface $request, $middleware, $handler, $arguments = [] ); 39 | 40 | /** 41 | * Return a response for the given request. 42 | * 43 | * @param RequestInterface $request 44 | * @param array $arguments 45 | * @return ResponseInterface|null 46 | */ 47 | public function handle( RequestInterface $request, $arguments = [] ); 48 | } 49 | -------------------------------------------------------------------------------- /src/Kernels/KernelsServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Kernels; 11 | 12 | use WPEmerge\ServiceProviders\ExtendsConfigTrait; 13 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 14 | 15 | /** 16 | * Provide old input dependencies. 17 | * 18 | * @codeCoverageIgnore 19 | */ 20 | class KernelsServiceProvider implements ServiceProviderInterface { 21 | use ExtendsConfigTrait; 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | public function register( $container ) { 27 | $this->extendConfig( $container, 'middleware', [ 28 | 'flash' => \WPEmerge\Flash\FlashMiddleware::class, 29 | 'old_input' => \WPEmerge\Input\OldInputMiddleware::class, 30 | 'csrf' => \WPEmerge\Csrf\CsrfMiddleware::class, 31 | 'user.logged_in' => \WPEmerge\Middleware\UserLoggedInMiddleware::class, 32 | 'user.logged_out' => \WPEmerge\Middleware\UserLoggedOutMiddleware::class, 33 | 'user.can' => \WPEmerge\Middleware\UserCanMiddleware::class, 34 | ] ); 35 | 36 | $this->extendConfig( $container, 'middleware_groups', [ 37 | 'wpemerge' => [ 38 | 'flash', 39 | 'old_input', 40 | ], 41 | 'global' => [], 42 | 'web' => [], 43 | 'ajax' => [], 44 | 'admin' => [], 45 | ] ); 46 | 47 | $this->extendConfig( $container, 'middleware_priority', [] ); 48 | 49 | $container[ WPEMERGE_WORDPRESS_HTTP_KERNEL_KEY ] = function ( $c ) { 50 | $kernel = new HttpKernel( 51 | $c, 52 | $c[ WPEMERGE_APPLICATION_GENERIC_FACTORY_KEY ], 53 | $c[ WPEMERGE_HELPERS_HANDLER_FACTORY_KEY ], 54 | $c[ WPEMERGE_RESPONSE_SERVICE_KEY ], 55 | $c[ WPEMERGE_REQUEST_KEY ], 56 | $c[ WPEMERGE_ROUTING_ROUTER_KEY ], 57 | $c[ WPEMERGE_VIEW_SERVICE_KEY ], 58 | $c[ WPEMERGE_EXCEPTIONS_ERROR_HANDLER_KEY ] 59 | ); 60 | 61 | $kernel->setMiddleware( $c[ WPEMERGE_CONFIG_KEY ]['middleware'] ); 62 | $kernel->setMiddlewareGroups( $c[ WPEMERGE_CONFIG_KEY ]['middleware_groups'] ); 63 | $kernel->setMiddlewarePriority( $c[ WPEMERGE_CONFIG_KEY ]['middleware_priority'] ); 64 | 65 | return $kernel; 66 | }; 67 | 68 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 69 | 70 | $app->alias( 'run', function () use ( $app ) { 71 | $kernel = $app->resolve( WPEMERGE_WORDPRESS_HTTP_KERNEL_KEY ); 72 | return call_user_func_array( [$kernel, 'run'], func_get_args() ); 73 | } ); 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public function bootstrap( $container ) { 80 | // Nothing to bootstrap. 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Middleware/ControllerMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use WPEmerge; 13 | 14 | /** 15 | * Redirect users who do not have a capability to a specific URL. 16 | */ 17 | class ControllerMiddleware { 18 | /** 19 | * Middleware. 20 | * 21 | * @var string[] 22 | */ 23 | protected $middleware = []; 24 | 25 | /** 26 | * Methods the middleware applies to. 27 | * 28 | * @var string[] 29 | */ 30 | protected $whitelist = []; 31 | 32 | /** 33 | * Methods the middleware does not apply to. 34 | * 35 | * @var string[] 36 | */ 37 | protected $blacklist = []; 38 | 39 | /** 40 | * Constructor. 41 | * 42 | * @codeCoverageIgnore 43 | * @param string|string[] $middleware 44 | */ 45 | public function __construct( $middleware ) { 46 | $this->middleware = (array) $middleware; 47 | } 48 | 49 | /** 50 | * Get middleware. 51 | * 52 | * @codeCoverageIgnore 53 | * @return string[] 54 | */ 55 | public function get() { 56 | return $this->middleware; 57 | } 58 | 59 | /** 60 | * Set methods the middleware should apply to. 61 | * 62 | * @codeCoverageIgnore 63 | * @param string|string[] $methods 64 | * @return static 65 | */ 66 | public function only( $methods ) { 67 | $this->whitelist = (array) $methods; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Set methods the middleware should not apply to. 74 | * 75 | * @codeCoverageIgnore 76 | * @param string|string[] $methods 77 | * @return static 78 | */ 79 | public function except( $methods ) { 80 | $this->blacklist = (array) $methods; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Get whether the middleware applies to the specified method. 87 | * 88 | * @param string $method 89 | * @return boolean 90 | */ 91 | public function appliesTo( $method ) { 92 | if ( in_array( $method, $this->blacklist, true ) ) { 93 | return false; 94 | } 95 | 96 | if ( empty( $this->whitelist ) ) { 97 | return true; 98 | } 99 | 100 | return in_array( $method, $this->whitelist, true ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Middleware/ExecutesMiddlewareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use Closure; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Requests\RequestInterface; 15 | 16 | /** 17 | * Executes middleware. 18 | */ 19 | trait ExecutesMiddlewareTrait { 20 | /** 21 | * Make a middleware class instance. 22 | * 23 | * @param string $class 24 | * @return object 25 | */ 26 | abstract protected function makeMiddleware( $class ); 27 | 28 | /** 29 | * Execute an array of middleware recursively (last in, first out). 30 | * 31 | * @param string[][] $middleware 32 | * @param RequestInterface $request 33 | * @param Closure $next 34 | * @return ResponseInterface 35 | */ 36 | protected function executeMiddleware( $middleware, RequestInterface $request, Closure $next ) { 37 | $top_middleware = array_shift( $middleware ); 38 | 39 | if ( $top_middleware === null ) { 40 | return $next( $request ); 41 | } 42 | 43 | $top_middleware_next = function ( $request ) use ( $middleware, $next ) { 44 | return $this->executeMiddleware( $middleware, $request, $next ); 45 | }; 46 | 47 | $instance = $this->makeMiddleware( $top_middleware[0] ); 48 | $arguments = array_merge( 49 | [$request, $top_middleware_next], 50 | array_slice( $top_middleware, 1 ) 51 | ); 52 | 53 | return call_user_func_array( [$instance, 'handle'], $arguments ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Middleware/HasControllerMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | /** 13 | * Interface for HasControllerMiddlewareTrait. 14 | */ 15 | interface HasControllerMiddlewareInterface { 16 | /** 17 | * Get middleware. 18 | * 19 | * @param string $method 20 | * @return string[] 21 | */ 22 | public function getMiddleware( $method ); 23 | 24 | /** 25 | * Add middleware. 26 | * 27 | * @param string|string[] $middleware 28 | * @return ControllerMiddleware 29 | */ 30 | public function addMiddleware( $middleware ); 31 | 32 | /** 33 | * Fluent alias for addMiddleware(). 34 | * 35 | * @param string|string[] $middleware 36 | * @return ControllerMiddleware 37 | */ 38 | public function middleware( $middleware ); 39 | } 40 | -------------------------------------------------------------------------------- /src/Middleware/HasControllerMiddlewareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | /** 13 | * Allow objects to have controller middleware. 14 | */ 15 | trait HasControllerMiddlewareTrait { 16 | /** 17 | * Array of middleware. 18 | * 19 | * @var ControllerMiddleware[] 20 | */ 21 | protected $middleware = []; 22 | 23 | /** 24 | * Get middleware. 25 | * 26 | * @param string $method 27 | * @return string[] 28 | */ 29 | public function getMiddleware( $method ) { 30 | $middleware = array_filter( $this->middleware, function ( ControllerMiddleware $middleware ) use ( $method ) { 31 | return $middleware->appliesTo( $method ); 32 | } ); 33 | 34 | $middleware = array_map( function ( ControllerMiddleware $middleware ) { 35 | return $middleware->get(); 36 | }, $middleware ); 37 | 38 | if ( ! empty( $middleware ) ) { 39 | $middleware = call_user_func_array( 'array_merge', $middleware ); 40 | } 41 | 42 | return $middleware; 43 | } 44 | 45 | /** 46 | * Add middleware. 47 | * 48 | * @param string|string[] $middleware 49 | * @return ControllerMiddleware 50 | */ 51 | public function addMiddleware( $middleware ) { 52 | $controller_middleware = new ControllerMiddleware( $middleware ); 53 | 54 | $this->middleware = array_merge( 55 | $this->middleware, 56 | [$controller_middleware] 57 | ); 58 | 59 | return $controller_middleware; 60 | } 61 | 62 | /** 63 | * Fluent alias for addMiddleware(). 64 | * 65 | * @codeCoverageIgnore 66 | * @param string|string[] $middleware 67 | * @return ControllerMiddleware 68 | */ 69 | public function middleware( $middleware ) { 70 | return call_user_func_array( [$this, 'addMiddleware'], func_get_args() ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Middleware/HasMiddlewareDefinitionsInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | /** 13 | * Provide middleware definitions. 14 | */ 15 | interface HasMiddlewareDefinitionsInterface { 16 | /** 17 | * Register middleware. 18 | * 19 | * @codeCoverageIgnore 20 | * @param array $middleware 21 | * @return void 22 | */ 23 | public function setMiddleware( $middleware ); 24 | 25 | /** 26 | * Register middleware groups. 27 | * 28 | * @codeCoverageIgnore 29 | * @param array $middleware_groups 30 | * @return void 31 | */ 32 | public function setMiddlewareGroups( $middleware_groups ); 33 | 34 | /** 35 | * Filter array of middleware into a unique set. 36 | * 37 | * @param array[] $middleware 38 | * @return string[] 39 | */ 40 | public function uniqueMiddleware( $middleware ); 41 | 42 | /** 43 | * Expand array of middleware into an array of fully qualified class names. 44 | * 45 | * @param string[] $middleware 46 | * @return array[] 47 | */ 48 | public function expandMiddleware( $middleware ); 49 | 50 | /** 51 | * Expand a middleware group into an array of fully qualified class names. 52 | * 53 | * @param string $group 54 | * @return array[] 55 | */ 56 | public function expandMiddlewareGroup( $group ); 57 | 58 | /** 59 | * Expand middleware into an array of fully qualified class names and any companion arguments. 60 | * 61 | * @param string $middleware 62 | * @return array[] 63 | */ 64 | public function expandMiddlewareMolecule( $middleware ); 65 | 66 | /** 67 | * Expand a single middleware a fully qualified class name. 68 | * 69 | * @param string $middleware 70 | * @return string 71 | */ 72 | public function expandMiddlewareAtom( $middleware ); 73 | } 74 | -------------------------------------------------------------------------------- /src/Middleware/HasMiddlewareDefinitionsTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use WPEmerge\Exceptions\ConfigurationException; 13 | 14 | /** 15 | * Provide middleware definitions. 16 | */ 17 | trait HasMiddlewareDefinitionsTrait { 18 | /** 19 | * Middleware available to the application. 20 | * 21 | * @var array 22 | */ 23 | protected $middleware = []; 24 | 25 | /** 26 | * Middleware groups. 27 | * 28 | * @var array 29 | */ 30 | protected $middleware_groups = []; 31 | 32 | /** 33 | * Middleware groups that should have the 'wpemerge' and 'global' groups prepended to them. 34 | * 35 | * @var string[] 36 | */ 37 | protected $prepend_special_groups_to = [ 38 | 'web', 39 | 'admin', 40 | 'ajax', 41 | ]; 42 | 43 | /** 44 | * Register middleware. 45 | * 46 | * @codeCoverageIgnore 47 | * @param array $middleware 48 | * @return void 49 | */ 50 | public function setMiddleware( $middleware ) { 51 | $this->middleware = $middleware; 52 | } 53 | 54 | /** 55 | * Register middleware groups. 56 | * 57 | * @codeCoverageIgnore 58 | * @param array $middleware_groups 59 | * @return void 60 | */ 61 | public function setMiddlewareGroups( $middleware_groups ) { 62 | $this->middleware_groups = $middleware_groups; 63 | } 64 | 65 | /** 66 | * Filter array of middleware into a unique set. 67 | * 68 | * @param array[] $middleware 69 | * @return string[] 70 | */ 71 | public function uniqueMiddleware( $middleware ) { 72 | return array_values( array_unique( $middleware, SORT_REGULAR ) ); 73 | } 74 | 75 | /** 76 | * Expand array of middleware into an array of fully qualified class names. 77 | * 78 | * @param string[] $middleware 79 | * @return array[] 80 | */ 81 | public function expandMiddleware( $middleware ) { 82 | $classes = []; 83 | 84 | foreach ( $middleware as $item ) { 85 | $classes = array_merge( 86 | $classes, 87 | $this->expandMiddlewareMolecule( $item ) 88 | ); 89 | } 90 | 91 | return $classes; 92 | } 93 | 94 | /** 95 | * Expand a middleware group into an array of fully qualified class names. 96 | * 97 | * @param string $group 98 | * @return array[] 99 | */ 100 | public function expandMiddlewareGroup( $group ) { 101 | if ( ! isset( $this->middleware_groups[ $group ] ) ) { 102 | throw new ConfigurationException( 'Unknown middleware group "' . $group . '" used.' ); 103 | } 104 | 105 | $middleware = $this->middleware_groups[ $group ]; 106 | 107 | if ( in_array( $group, $this->prepend_special_groups_to, true ) ) { 108 | $middleware = array_merge( ['wpemerge', 'global'], $middleware ); 109 | } 110 | 111 | return $this->expandMiddleware( $middleware ); 112 | } 113 | 114 | /** 115 | * Expand middleware into an array of fully qualified class names and any companion arguments. 116 | * 117 | * @param string $middleware 118 | * @return array[] 119 | */ 120 | public function expandMiddlewareMolecule( $middleware ) { 121 | $pieces = explode( ':', $middleware, 2 ); 122 | 123 | if ( count( $pieces ) > 1 ) { 124 | return [array_merge( [$this->expandMiddlewareAtom( $pieces[0] )], explode( ',', $pieces[1] ) )]; 125 | } 126 | 127 | if ( isset( $this->middleware_groups[ $middleware ] ) ) { 128 | return $this->expandMiddlewareGroup( $middleware ); 129 | } 130 | 131 | return [[$this->expandMiddlewareAtom( $middleware )]]; 132 | } 133 | 134 | /** 135 | * Expand a single middleware a fully qualified class name. 136 | * 137 | * @param string $middleware 138 | * @return string 139 | */ 140 | public function expandMiddlewareAtom( $middleware ) { 141 | if ( isset( $this->middleware[ $middleware ] ) ) { 142 | return $this->middleware[ $middleware ]; 143 | } 144 | 145 | if ( class_exists( $middleware ) ) { 146 | return $middleware; 147 | } 148 | 149 | throw new ConfigurationException( 'Unknown middleware "' . $middleware . '" used.' ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Middleware/MiddlewareServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide middleware dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class MiddlewareServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ UserLoggedOutMiddleware::class ] = function ( $c ) { 25 | return new UserLoggedOutMiddleware( $c[ WPEMERGE_RESPONSE_SERVICE_KEY ] ); 26 | }; 27 | 28 | $container[ UserLoggedInMiddleware::class ] = function ( $c ) { 29 | return new UserLoggedInMiddleware( $c[ WPEMERGE_RESPONSE_SERVICE_KEY ] ); 30 | }; 31 | 32 | $container[ UserCanMiddleware::class ] = function ( $c ) { 33 | return new UserCanMiddleware( $c[ WPEMERGE_RESPONSE_SERVICE_KEY ] ); 34 | }; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function bootstrap( $container ) { 41 | // Nothing to bootstrap. 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/ReadsHandlerMiddlewareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use WPEmerge\Helpers\Handler; 13 | 14 | /** 15 | * Describes how a request is handled. 16 | */ 17 | trait ReadsHandlerMiddlewareTrait { 18 | /** 19 | * Get middleware registered with the given handler. 20 | * 21 | * @param Handler $handler 22 | * @return string[] 23 | */ 24 | protected function getHandlerMiddleware( Handler $handler ) { 25 | $instance = $handler->make(); 26 | 27 | if ( ! $instance instanceof HasControllerMiddlewareInterface ) { 28 | return []; 29 | } 30 | 31 | return $instance->getMiddleware( $handler->get()['method'] ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Middleware/UserCanMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use Closure; 13 | use WPEmerge\Requests\RequestInterface; 14 | use WPEmerge\Responses\ResponseService; 15 | 16 | /** 17 | * Redirect users who do not have a capability to a specific URL. 18 | */ 19 | class UserCanMiddleware { 20 | /** 21 | * Response service. 22 | * 23 | * @var ResponseService 24 | */ 25 | protected $response_service = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @codeCoverageIgnore 31 | * @param ResponseService $response_service 32 | */ 33 | public function __construct( ResponseService $response_service ) { 34 | $this->response_service = $response_service; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function handle( RequestInterface $request, Closure $next, $capability = '', $object_id = '0', $url = '' ) { 41 | $capability = apply_filters( 'wpemerge.middleware.user.can.capability', $capability, $request ); 42 | $object_id = apply_filters( 'wpemerge.middleware.user.can.object_id', (int) $object_id, $capability, $request ); 43 | $args = [$capability]; 44 | 45 | if ( $object_id !== 0 ) { 46 | $args[] = $object_id; 47 | } 48 | 49 | if ( call_user_func_array( 'current_user_can', $args ) ) { 50 | return $next( $request ); 51 | } 52 | 53 | if ( empty( $url ) ) { 54 | $url = home_url(); 55 | } 56 | 57 | $url = apply_filters( 'wpemerge.middleware.user.can.redirect_url', $url, $request ); 58 | 59 | return $this->response_service->redirect( $request )->to( $url ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Middleware/UserLoggedInMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use Closure; 13 | use WPEmerge\Requests\RequestInterface; 14 | use WPEmerge\Responses\ResponseService; 15 | 16 | /** 17 | * Redirect non-logged in users to a specific URL. 18 | */ 19 | class UserLoggedInMiddleware { 20 | /** 21 | * Response service. 22 | * 23 | * @var ResponseService 24 | */ 25 | protected $response_service = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @codeCoverageIgnore 31 | * @param ResponseService $response_service 32 | */ 33 | public function __construct( ResponseService $response_service ) { 34 | $this->response_service = $response_service; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function handle( RequestInterface $request, Closure $next, $url = '' ) { 41 | if ( is_user_logged_in() ) { 42 | return $next( $request ); 43 | } 44 | 45 | if ( empty( $url ) ) { 46 | $url = wp_login_url( $request->getUrl() ); 47 | } 48 | 49 | $url = apply_filters( 'wpemerge.middleware.user.logged_in.redirect_url', $url, $request ); 50 | 51 | return $this->response_service->redirect( $request )->to( $url ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Middleware/UserLoggedOutMiddleware.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Middleware; 11 | 12 | use Closure; 13 | use WPEmerge\Requests\RequestInterface; 14 | use WPEmerge\Responses\ResponseService; 15 | 16 | /** 17 | * Redirect logged in users to a specific URL. 18 | */ 19 | class UserLoggedOutMiddleware { 20 | /** 21 | * Response service. 22 | * 23 | * @var ResponseService 24 | */ 25 | protected $response_service = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @codeCoverageIgnore 31 | * @param ResponseService $response_service 32 | */ 33 | public function __construct( ResponseService $response_service ) { 34 | $this->response_service = $response_service; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function handle( RequestInterface $request, Closure $next, $url = '' ) { 41 | if ( ! is_user_logged_in() ) { 42 | return $next( $request ); 43 | } 44 | 45 | if ( empty( $url ) ) { 46 | $url = home_url(); 47 | } 48 | 49 | $url = apply_filters( 'wpemerge.middleware.user.logged_out.redirect_url', $url, $request ); 50 | 51 | return $this->response_service->redirect( $request )->to( $url ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Requests/Request.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Requests; 11 | 12 | use GuzzleHttp\Psr7\ServerRequest; 13 | use WPEmerge\Support\Arr; 14 | 15 | /** 16 | * A representation of a request to the server. 17 | */ 18 | class Request extends ServerRequest implements RequestInterface { 19 | /** 20 | * @codeCoverageIgnore 21 | * {@inheritDoc} 22 | * @return static 23 | */ 24 | public static function fromGlobals() { 25 | $request = parent::fromGlobals(); 26 | $new = new self( 27 | $request->getMethod(), 28 | $request->getUri(), 29 | $request->getHeaders(), 30 | $request->getBody(), 31 | $request->getProtocolVersion(), 32 | $request->getServerParams() 33 | ); 34 | 35 | return $new 36 | ->withCookieParams( $_COOKIE ) 37 | ->withQueryParams( $_GET ) 38 | ->withParsedBody( $_POST ) 39 | ->withUploadedFiles( static::normalizeFiles( $_FILES ) ); 40 | } 41 | 42 | /** 43 | * @codeCoverageIgnore 44 | * {@inheritDoc} 45 | */ 46 | public function getUrl() { 47 | return $this->getUri(); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | protected function getMethodOverride( $default ) { 54 | $valid_overrides = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; 55 | $override = $default; 56 | 57 | $header_override = (string) $this->getHeaderLine( 'X-HTTP-METHOD-OVERRIDE' ); 58 | if ( ! empty( $header_override ) ) { 59 | $override = strtoupper( $header_override ); 60 | } 61 | 62 | $body_override = (string) $this->body( '_method', '' ); 63 | if ( ! empty( $body_override ) ) { 64 | $override = strtoupper( $body_override ); 65 | } 66 | 67 | if ( in_array( $override, $valid_overrides, true ) ) { 68 | return $override; 69 | } 70 | 71 | return $default; 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function getMethod() { 78 | $method = parent::getMethod(); 79 | 80 | if ( $method === 'POST' ) { 81 | $method = $this->getMethodOverride( $method ); 82 | } 83 | 84 | return $method; 85 | } 86 | 87 | /** 88 | * {@inheritDoc} 89 | */ 90 | public function isGet() { 91 | return $this->getMethod() === 'GET'; 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | public function isHead() { 98 | return $this->getMethod() === 'HEAD'; 99 | } 100 | 101 | /** 102 | * {@inheritDoc} 103 | */ 104 | public function isPost() { 105 | return $this->getMethod() === 'POST'; 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function isPut() { 112 | return $this->getMethod() === 'PUT'; 113 | } 114 | 115 | /** 116 | * {@inheritDoc} 117 | */ 118 | public function isPatch() { 119 | return $this->getMethod() === 'PATCH'; 120 | } 121 | 122 | /** 123 | * {@inheritDoc} 124 | */ 125 | public function isDelete() { 126 | return $this->getMethod() === 'DELETE'; 127 | } 128 | 129 | /** 130 | * {@inheritDoc} 131 | */ 132 | public function isOptions() { 133 | return $this->getMethod() === 'OPTIONS'; 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | */ 139 | public function isReadVerb() { 140 | return in_array( $this->getMethod(), ['GET', 'HEAD', 'OPTIONS'] ); 141 | } 142 | 143 | /** 144 | * {@inheritDoc} 145 | */ 146 | public function isAjax() { 147 | return strtolower( $this->getHeaderLine( 'X-Requested-With' ) ) === 'xmlhttprequest'; 148 | } 149 | 150 | /** 151 | * Get all values or a single one from an input type. 152 | * 153 | * @param array $source 154 | * @param string $key 155 | * @param mixed $default 156 | * @return mixed 157 | */ 158 | protected function get( $source, $key = '', $default = null ) { 159 | if ( empty( $key ) ) { 160 | return $source; 161 | } 162 | 163 | return Arr::get( $source, $key, $default ); 164 | } 165 | 166 | /** 167 | * {@inheritDoc} 168 | * @see ::get() 169 | */ 170 | public function attributes( $key = '', $default = null ) { 171 | return call_user_func( [$this, 'get'], $this->getAttributes(), $key, $default ); 172 | } 173 | 174 | /** 175 | * {@inheritDoc} 176 | * @see ::get() 177 | */ 178 | public function query( $key = '', $default = null ) { 179 | return call_user_func( [$this, 'get'], $this->getQueryParams(), $key, $default ); 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | * @see ::get() 185 | */ 186 | public function body( $key = '', $default = null ) { 187 | return call_user_func( [$this, 'get'], $this->getParsedBody(), $key, $default ); 188 | } 189 | 190 | /** 191 | * {@inheritDoc} 192 | * @see ::get() 193 | */ 194 | public function cookies( $key = '', $default = null ) { 195 | return call_user_func( [$this, 'get'], $this->getCookieParams(), $key, $default ); 196 | } 197 | 198 | /** 199 | * {@inheritDoc} 200 | * @see ::get() 201 | */ 202 | public function files( $key = '', $default = null ) { 203 | return call_user_func( [$this, 'get'], $this->getUploadedFiles(), $key, $default ); 204 | } 205 | 206 | /** 207 | * {@inheritDoc} 208 | * @see ::get() 209 | */ 210 | public function server( $key = '', $default = null ) { 211 | return call_user_func( [$this, 'get'], $this->getServerParams(), $key, $default ); 212 | } 213 | 214 | /** 215 | * {@inheritDoc} 216 | * @see ::get() 217 | */ 218 | public function headers( $key = '', $default = null ) { 219 | return call_user_func( [$this, 'get'], $this->getHeaders(), $key, $default ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Requests/RequestInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Requests; 11 | 12 | use Psr\Http\Message\ServerRequestInterface; 13 | 14 | /** 15 | * A representation of a request to the server. 16 | */ 17 | interface RequestInterface extends ServerRequestInterface { 18 | /** 19 | * Alias for ::getUri(). 20 | * Even though URI and URL are slightly different things this alias returns the URI for simplicity/familiarity. 21 | * 22 | * @return string 23 | */ 24 | public function getUrl(); 25 | 26 | /** 27 | * Check if the request method is GET. 28 | * 29 | * @return boolean 30 | */ 31 | public function isGet(); 32 | 33 | /** 34 | * Check if the request method is HEAD. 35 | * 36 | * @return boolean 37 | */ 38 | public function isHead(); 39 | 40 | /** 41 | * Check if the request method is POST. 42 | * 43 | * @return boolean 44 | */ 45 | public function isPost(); 46 | 47 | /** 48 | * Check if the request method is PUT. 49 | * 50 | * @return boolean 51 | */ 52 | public function isPut(); 53 | 54 | /** 55 | * Check if the request method is PATCH. 56 | * 57 | * @return boolean 58 | */ 59 | public function isPatch(); 60 | 61 | /** 62 | * Check if the request method is DELETE. 63 | * 64 | * @return boolean 65 | */ 66 | public function isDelete(); 67 | 68 | /** 69 | * Check if the request method is OPTIONS. 70 | * 71 | * @return boolean 72 | */ 73 | public function isOptions(); 74 | 75 | /** 76 | * Check if the request method is a "read" verb. 77 | * 78 | * @return boolean 79 | */ 80 | public function isReadVerb(); 81 | 82 | /** 83 | * Check if the request is an ajax request. 84 | * 85 | * @return boolean 86 | */ 87 | public function isAjax(); 88 | 89 | /** 90 | * Get a value from the request attributes. 91 | * 92 | * @param string $key 93 | * @param mixed $default 94 | * @return mixed 95 | */ 96 | public function attributes( $key = '', $default = null ); 97 | 98 | /** 99 | * Get a value from the request query (i.e. $_GET). 100 | * 101 | * @param string $key 102 | * @param mixed $default 103 | * @return mixed 104 | */ 105 | public function query( $key = '', $default = null ); 106 | 107 | /** 108 | * Get a value from the request body (i.e. $_POST). 109 | * 110 | * @param string $key 111 | * @param mixed $default 112 | * @return mixed 113 | */ 114 | public function body( $key = '', $default = null ); 115 | 116 | /** 117 | * Get a value from the COOKIE parameters. 118 | * 119 | * @param string $key 120 | * @param mixed $default 121 | * @return mixed 122 | */ 123 | public function cookies( $key = '', $default = null ); 124 | 125 | /** 126 | * Get a value from the FILES parameters. 127 | * 128 | * @param string $key 129 | * @param mixed $default 130 | * @return mixed 131 | */ 132 | public function files( $key = '', $default = null ); 133 | 134 | /** 135 | * Get a value from the SERVER parameters. 136 | * 137 | * @param string $key 138 | * @param mixed $default 139 | * @return mixed 140 | */ 141 | public function server( $key = '', $default = null ); 142 | 143 | /** 144 | * Get a value from the headers. 145 | * 146 | * @param string $key 147 | * @param mixed $default 148 | * @return mixed 149 | */ 150 | public function headers( $key = '', $default = null ); 151 | } 152 | -------------------------------------------------------------------------------- /src/Requests/RequestsServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Requests; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide request dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class RequestsServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ WPEMERGE_REQUEST_KEY ] = function () { 25 | return Request::fromGlobals(); 26 | }; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public function bootstrap( $container ) { 33 | // Nothing to bootstrap. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Responses/ConvertsToResponseTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Responses; 11 | 12 | /** 13 | * Converts values to a response. 14 | */ 15 | trait ConvertsToResponseTrait { 16 | /** 17 | * Get a Response Service instance. 18 | * 19 | * @return ResponseService 20 | */ 21 | abstract protected function getResponseService(); 22 | 23 | /** 24 | * Convert a user returned response to a ResponseInterface instance if possible. 25 | * Return the original value if unsupported. 26 | * 27 | * @param mixed $response 28 | * @return mixed 29 | */ 30 | protected function toResponse( $response ) { 31 | if ( is_string( $response ) ) { 32 | return $this->getResponseService()->output( $response ); 33 | } 34 | 35 | if ( is_array( $response ) ) { 36 | return $this->getResponseService()->json( $response ); 37 | } 38 | 39 | if ( $response instanceof ResponsableInterface ) { 40 | return $response->toResponse(); 41 | } 42 | 43 | return $response; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Responses/RedirectResponse.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Responses; 11 | 12 | use GuzzleHttp\Psr7\Response as Psr7Response; 13 | use Psr\Http\Message\ResponseInterface; 14 | use WPEmerge\Requests\RequestInterface; 15 | 16 | /** 17 | * A collection of tools for the creation of responses 18 | */ 19 | class RedirectResponse extends Psr7Response { 20 | /** 21 | * Current request. 22 | * 23 | * @var RequestInterface 24 | */ 25 | protected $request = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @codeCoverageIgnore 31 | * @param RequestInterface $request 32 | */ 33 | public function __construct( RequestInterface $request ) { 34 | parent::__construct(); 35 | $this->request = $request; 36 | } 37 | 38 | /** 39 | * Get a response redirecting to a specific url. 40 | * 41 | * @param string $url 42 | * @param integer $status 43 | * @return ResponseInterface 44 | */ 45 | public function to( $url, $status = 302 ) { 46 | return $this 47 | ->withHeader( 'Location', $url ) 48 | ->withStatus( $status ); 49 | } 50 | 51 | /** 52 | * Get a response redirecting back to the referrer or a fallback. 53 | * 54 | * @param string $fallback 55 | * @param integer $status 56 | * @return ResponseInterface 57 | */ 58 | public function back( $fallback = '', $status = 302 ) { 59 | $url = $this->request->getHeaderLine( 'Referer' ); 60 | 61 | if ( empty( $url ) ) { 62 | $url = $fallback; 63 | } 64 | 65 | if ( empty( $url ) ) { 66 | $url = $this->request->getUrl(); 67 | } 68 | 69 | return $this->to( $url, $status ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Responses/ResponsableInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Responses; 11 | 12 | use Psr\Http\Message\ResponseInterface; 13 | 14 | interface ResponsableInterface { 15 | /** 16 | * Convert to Psr\Http\Message\ResponseInterface. 17 | * 18 | * @return ResponseInterface 19 | */ 20 | public function toResponse(); 21 | } 22 | -------------------------------------------------------------------------------- /src/Responses/ResponseService.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Responses; 11 | 12 | use GuzzleHttp\Psr7; 13 | use GuzzleHttp\Psr7\Response as Psr7Response; 14 | use WPEmerge\Requests\RequestInterface; 15 | use Psr\Http\Message\ResponseInterface; 16 | use Psr\Http\Message\StreamInterface; 17 | use WPEmerge\View\ViewService; 18 | 19 | /** 20 | * A collection of tools for the creation of responses. 21 | */ 22 | class ResponseService { 23 | /** 24 | * Current request. 25 | * 26 | * @var RequestInterface 27 | */ 28 | protected $request = null; 29 | 30 | /** 31 | * View service. 32 | * 33 | * @var ViewService 34 | */ 35 | protected $view_service = null; 36 | 37 | /** 38 | * Constructor. 39 | * 40 | * @codeCoverageIgnore 41 | * @param RequestInterface $request 42 | * @param ViewService $view_service 43 | */ 44 | public function __construct( RequestInterface $request, ViewService $view_service ) { 45 | $this->request = $request; 46 | $this->view_service = $view_service; 47 | } 48 | 49 | /** 50 | * Send output based on a response object. 51 | * @credit heavily modified version of slimphp/slim - Slim/App.php 52 | * 53 | * @codeCoverageIgnore 54 | * @param ResponseInterface $response 55 | * @return void 56 | */ 57 | public function respond( ResponseInterface $response ) { 58 | if ( ! headers_sent() ) { 59 | $this->sendHeaders( $response ); 60 | } 61 | $this->sendBody( $response ); 62 | } 63 | 64 | /** 65 | * Send a request's headers to the client. 66 | * 67 | * @codeCoverageIgnore 68 | * @param ResponseInterface $response 69 | * @return void 70 | */ 71 | public function sendHeaders( ResponseInterface $response ) { 72 | // Status 73 | header( sprintf( 74 | 'HTTP/%s %s %s', 75 | $response->getProtocolVersion(), 76 | $response->getStatusCode(), 77 | $response->getReasonPhrase() 78 | ) ); 79 | 80 | // Headers 81 | foreach ( $response->getHeaders() as $name => $values ) { 82 | foreach ( $values as $i => $value ) { 83 | header( sprintf( '%s: %s', $name, $value ), $i === 0 ); 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Get a response's body stream so it is ready to be read. 90 | * 91 | * @codeCoverageIgnore 92 | * @param ResponseInterface $response 93 | * @return StreamInterface 94 | */ 95 | protected function getBody( ResponseInterface $response ) { 96 | $body = $response->getBody(); 97 | if ( $body->isSeekable() ) { 98 | $body->rewind(); 99 | } 100 | return $body; 101 | } 102 | 103 | /** 104 | * Get a response's body's content length. 105 | * 106 | * @codeCoverageIgnore 107 | * @param ResponseInterface $response 108 | * @return integer 109 | */ 110 | protected function getBodyContentLength( ResponseInterface $response ) { 111 | $content_length = $response->getHeaderLine( 'Content-Length' ); 112 | 113 | if ( ! $content_length ) { 114 | $body = $this->getBody( $response ); 115 | $content_length = $body->getSize(); 116 | } 117 | 118 | if ( ! is_numeric( $content_length ) ) { 119 | $content_length = 0; 120 | } 121 | 122 | return (integer) $content_length; 123 | } 124 | 125 | /** 126 | * Send a request's body to the client. 127 | * 128 | * @codeCoverageIgnore 129 | * @param ResponseInterface $response 130 | * @param integer $chunk_size 131 | * @return void 132 | */ 133 | public function sendBody( ResponseInterface $response, $chunk_size = 4096 ) { 134 | $body = $this->getBody( $response ); 135 | $content_length = $this->getBodyContentLength( $response ); 136 | 137 | if ( $content_length > 0 ) { 138 | $this->sendBodyWithLength( $body, $content_length, $chunk_size ); 139 | } else { 140 | $this->sendBodyWithoutLength( $body, $chunk_size ); 141 | } 142 | } 143 | 144 | /** 145 | * Send a body with an unknown length to the client. 146 | * 147 | * @codeCoverageIgnore 148 | * @param StreamInterface $body 149 | * @param integer $chunk_size 150 | * @return void 151 | */ 152 | protected function sendBodyWithoutLength( StreamInterface $body, $chunk_size ) { 153 | while ( connection_status() === CONNECTION_NORMAL && ! $body->eof() ) { 154 | echo $body->read( $chunk_size ); 155 | } 156 | } 157 | 158 | /** 159 | * Send a body with a known length to the client. 160 | * 161 | * @codeCoverageIgnore 162 | * @param StreamInterface $body 163 | * @param integer $length 164 | * @param integer $chunk_size 165 | * @return void 166 | */ 167 | protected function sendBodyWithLength( StreamInterface $body, $length, $chunk_size ) { 168 | $content_left = $length; 169 | 170 | while ( connection_status() === CONNECTION_NORMAL && $content_left > 0 ) { 171 | $read = min( $chunk_size, $content_left ); 172 | 173 | if ( $read <= 0 ) { 174 | break; 175 | } 176 | 177 | echo $body->read( $read ); 178 | 179 | $content_left -= $read; 180 | } 181 | } 182 | 183 | /** 184 | * Create a new response object. 185 | * 186 | * @return ResponseInterface 187 | */ 188 | public function response() { 189 | return new Psr7Response(); 190 | } 191 | 192 | /** 193 | * Get a cloned response with the passed string as the body. 194 | * 195 | * @param string $output 196 | * @return ResponseInterface 197 | */ 198 | public function output( $output ) { 199 | $response = $this->response(); 200 | $response = $response->withBody( Psr7\stream_for( $output ) ); 201 | return $response; 202 | } 203 | 204 | /** 205 | * Get a cloned response, json encoding the passed data as the body. 206 | * 207 | * @param mixed $data 208 | * @return ResponseInterface 209 | */ 210 | public function json( $data ) { 211 | $response = $this->response(); 212 | $response = $response->withHeader( 'Content-Type', 'application/json' ); 213 | $response = $response->withBody( Psr7\stream_for( wp_json_encode( $data ) ) ); 214 | return $response; 215 | } 216 | 217 | /** 218 | * Get a cloned response, with location and status headers. 219 | * 220 | * @param RequestInterface|null $request 221 | * @return RedirectResponse 222 | */ 223 | public function redirect( RequestInterface $request = null ) { 224 | $request = $request ? $request : $this->request; 225 | return new RedirectResponse( $request ); 226 | } 227 | 228 | /** 229 | * Get an error response, with status headers and rendering a suitable view as the body. 230 | * 231 | * @param integer $status 232 | * @return ResponseInterface 233 | */ 234 | public function error( $status ) { 235 | $views = [$status, 'error', 'index']; 236 | 237 | if ( is_admin() ) { 238 | $views = array_merge( 239 | array_map( function ( $view ) { 240 | return $view . '-' . ( wp_doing_ajax() ? 'ajax' : 'admin' ); 241 | }, $views ), 242 | $views 243 | ); 244 | } 245 | 246 | return $this->view_service->make( $views ) 247 | ->toResponse() 248 | ->withStatus( $status ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Responses/ResponsesServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Responses; 11 | 12 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 13 | 14 | /** 15 | * Provide responses dependencies. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class ResponsesServiceProvider implements ServiceProviderInterface { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function register( $container ) { 24 | $container[ WPEMERGE_RESPONSE_SERVICE_KEY ] = function ( $c ) { 25 | return new ResponseService( $c[ WPEMERGE_REQUEST_KEY ], $c[ WPEMERGE_VIEW_SERVICE_KEY ] ); 26 | }; 27 | 28 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 29 | $app->alias( 'responses', WPEMERGE_RESPONSE_SERVICE_KEY ); 30 | 31 | $app->alias( 'response', function () use ( $app ) { 32 | return call_user_func_array( [$app->responses(), 'response'], func_get_args() ); 33 | } ); 34 | 35 | $app->alias( 'output', function () use ( $app ) { 36 | return call_user_func_array( [$app->responses(), 'output'], func_get_args() ); 37 | } ); 38 | 39 | $app->alias( 'json', function () use ( $app ) { 40 | return call_user_func_array( [$app->responses(), 'json'], func_get_args() ); 41 | } ); 42 | 43 | $app->alias( 'redirect', function () use ( $app ) { 44 | return call_user_func_array( [$app->responses(), 'redirect'], func_get_args() ); 45 | } ); 46 | 47 | $app->alias( 'error', function () use ( $app ) { 48 | return call_user_func_array( [$app->responses(), 'error'], func_get_args() ); 49 | } ); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function bootstrap( $container ) { 56 | // Nothing to bootstrap. 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Routing/Conditions/AdminCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current ajax action. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class AdminCondition implements ConditionInterface, UrlableInterface { 20 | /** 21 | * Menu slug. 22 | * 23 | * @var string 24 | */ 25 | protected $menu = ''; 26 | 27 | /** 28 | * Parent menu slug. 29 | * 30 | * @var string 31 | */ 32 | protected $parent_menu = ''; 33 | 34 | /** 35 | * Constructor 36 | * 37 | * @codeCoverageIgnore 38 | * @param string $menu 39 | * @param string $parent_menu 40 | */ 41 | public function __construct( $menu, $parent_menu = '' ) { 42 | $this->menu = $menu; 43 | $this->parent_menu = $parent_menu; 44 | } 45 | 46 | /** 47 | * Check if the admin page requirement matches. 48 | * 49 | * @return boolean 50 | */ 51 | protected function isAdminPage() { 52 | return is_admin() && ! wp_doing_ajax(); 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function isSatisfied( RequestInterface $request ) { 59 | if ( ! $this->isAdminPage() ) { 60 | return false; 61 | } 62 | 63 | $screen = get_current_screen(); 64 | 65 | if ( ! $screen ) { 66 | return false; 67 | } 68 | 69 | return $screen->id === get_plugin_page_hookname( $this->menu, $this->parent_menu ); 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function getArguments( RequestInterface $request ) { 76 | return [ 77 | 'menu' => $this->menu, 78 | 'parent_menu' => $this->parent_menu, 79 | 'hook' => get_plugin_page_hookname( $this->menu, $this->parent_menu ) 80 | ]; 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function toUrl( $arguments = [] ) { 87 | if ( ! function_exists( 'menu_page_url' ) ) { 88 | // Attempted to resolve an admin url while not in the admin which can only happen 89 | // by mistake as admin routes are defined in the admin context only. 90 | return home_url( '/' ); 91 | } 92 | 93 | return menu_page_url( $this->menu, false ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Routing/Conditions/AjaxCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current ajax action. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class AjaxCondition implements ConditionInterface, UrlableInterface { 20 | /** 21 | * Ajax action to check against. 22 | * 23 | * @var string 24 | */ 25 | protected $action = ''; 26 | 27 | /** 28 | * Flag whether to check against ajax actions which run for authenticated users. 29 | * 30 | * @var boolean 31 | */ 32 | protected $private = true; 33 | 34 | /** 35 | * Flag whether to check against ajax actions which run for unauthenticated users. 36 | * 37 | * @var boolean 38 | */ 39 | protected $public = false; 40 | 41 | /** 42 | * Constructor 43 | * 44 | * @codeCoverageIgnore 45 | * @param string $action 46 | * @param boolean $private 47 | * @param boolean $public 48 | */ 49 | public function __construct( $action, $private = true, $public = false ) { 50 | $this->action = $action; 51 | $this->private = $private; 52 | $this->public = $public; 53 | } 54 | 55 | /** 56 | * Check if the private authentication requirement matches. 57 | * 58 | * @return boolean 59 | */ 60 | protected function matchesPrivateRequirement() { 61 | return $this->private && is_user_logged_in(); 62 | } 63 | 64 | /** 65 | * Check if the public authentication requirement matches. 66 | * 67 | * @return boolean 68 | */ 69 | protected function matchesPublicRequirement() { 70 | return $this->public && ! is_user_logged_in(); 71 | } 72 | 73 | /** 74 | * Check if the ajax action matches the requirement. 75 | * 76 | * @param RequestInterface $request 77 | * @return boolean 78 | */ 79 | protected function matchesActionRequirement( RequestInterface $request ) { 80 | return $this->action === $request->body( 'action', $request->query( 'action' ) ); 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function isSatisfied( RequestInterface $request ) { 87 | if ( ! wp_doing_ajax() ) { 88 | return false; 89 | } 90 | 91 | if ( ! $this->matchesActionRequirement( $request ) ) { 92 | return false; 93 | } 94 | 95 | return $this->matchesPrivateRequirement() || $this->matchesPublicRequirement(); 96 | } 97 | 98 | /** 99 | * {@inheritDoc} 100 | */ 101 | public function getArguments( RequestInterface $request ) { 102 | return ['action' => $this->action]; 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | public function toUrl( $arguments = [] ) { 109 | return add_query_arg( 'action', $this->action, self_admin_url( 'admin-ajax.php' ) ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Routing/Conditions/CanFilterQueryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | /** 13 | * Interface signifying that an object can filter the main WordPress query. 14 | */ 15 | interface CanFilterQueryInterface { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Routing/Conditions/ConditionInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Interface that condition types must implement 16 | */ 17 | interface ConditionInterface { 18 | /** 19 | * Get whether the condition is satisfied 20 | * 21 | * @param RequestInterface $request 22 | * @return boolean 23 | */ 24 | public function isSatisfied( RequestInterface $request ); 25 | 26 | /** 27 | * Get an array of arguments for use in request 28 | * 29 | * @param RequestInterface $request 30 | * @return array 31 | */ 32 | public function getArguments( RequestInterface $request ); 33 | } 34 | -------------------------------------------------------------------------------- /src/Routing/Conditions/CustomCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against a custom callable. 16 | */ 17 | class CustomCondition implements ConditionInterface { 18 | /** 19 | * Callable to use 20 | * 21 | * @var callable 22 | */ 23 | protected $callable = null; 24 | 25 | /** 26 | * Arguments to pass to the callable and controller 27 | * 28 | * @var array 29 | */ 30 | protected $arguments = []; 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @codeCoverageIgnore 36 | * @param callable $callable 37 | * @param mixed ,...$arguments 38 | */ 39 | public function __construct( $callable ) { 40 | $this->callable = $callable; 41 | $this->arguments = array_values( array_slice( func_get_args(), 1 ) ); 42 | } 43 | 44 | /** 45 | * Get the assigned callable 46 | * 47 | * @codeCoverageIgnore 48 | * @return callable 49 | */ 50 | public function getCallable() { 51 | return $this->callable; 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function isSatisfied( RequestInterface $request ) { 58 | return call_user_func_array( $this->callable, $this->arguments ); 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | public function getArguments( RequestInterface $request ) { 65 | return $this->arguments; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Routing/Conditions/MultipleCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against an array of conditions in an AND logical relationship. 16 | */ 17 | class MultipleCondition implements ConditionInterface { 18 | /** 19 | * Array of conditions to check. 20 | * 21 | * @var ConditionInterface[] 22 | */ 23 | protected $conditions = []; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @codeCoverageIgnore 29 | * @param ConditionInterface[] $conditions 30 | */ 31 | public function __construct( $conditions ) { 32 | $this->conditions = $conditions; 33 | } 34 | 35 | /** 36 | * Get all assigned conditions 37 | * 38 | * @codeCoverageIgnore 39 | * @return ConditionInterface[] 40 | */ 41 | public function getConditions() { 42 | return $this->conditions; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function isSatisfied( RequestInterface $request ) { 49 | foreach ( $this->conditions as $condition ) { 50 | if ( ! $condition->isSatisfied( $request ) ) { 51 | return false; 52 | } 53 | } 54 | return true; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getArguments( RequestInterface $request ) { 61 | $arguments = []; 62 | 63 | foreach ( $this->conditions as $condition ) { 64 | $arguments = array_merge( $arguments, $condition->getArguments( $request ) ); 65 | } 66 | 67 | return $arguments; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Routing/Conditions/NegateCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Negate another condition's result. 16 | */ 17 | class NegateCondition implements ConditionInterface { 18 | /** 19 | * Condition to negate. 20 | * 21 | * @var ConditionInterface 22 | */ 23 | protected $condition = null; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @codeCoverageIgnore 29 | * @param ConditionInterface $condition 30 | */ 31 | public function __construct( $condition ) { 32 | $this->condition = $condition; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function isSatisfied( RequestInterface $request ) { 39 | return ! $this->condition->isSatisfied( $request ); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getArguments( RequestInterface $request ) { 46 | return $this->condition->getArguments( $request ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Routing/Conditions/PostIdCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current post's id. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class PostIdCondition implements ConditionInterface, UrlableInterface { 20 | /** 21 | * Post id to check against 22 | * 23 | * @var integer 24 | */ 25 | protected $post_id = 0; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @codeCoverageIgnore 31 | * @param integer $post_id 32 | */ 33 | public function __construct( $post_id ) { 34 | $this->post_id = (int) $post_id; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function isSatisfied( RequestInterface $request ) { 41 | return ( is_singular() && $this->post_id === (int) get_the_ID() ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function getArguments( RequestInterface $request ) { 48 | return ['post_id' => $this->post_id]; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function toUrl( $arguments = [] ) { 55 | return get_permalink( $this->post_id ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Routing/Conditions/PostSlugCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current post's slug. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class PostSlugCondition implements ConditionInterface { 20 | /** 21 | * Post slug to check against 22 | * 23 | * @var string 24 | */ 25 | protected $post_slug = ''; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @codeCoverageIgnore 31 | * @param string $post_slug 32 | */ 33 | public function __construct( $post_slug ) { 34 | $this->post_slug = $post_slug; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function isSatisfied( RequestInterface $request ) { 41 | $post = get_post(); 42 | return ( is_singular() && $post && $this->post_slug === $post->post_name ); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getArguments( RequestInterface $request ) { 49 | return ['post_slug' => $this->post_slug]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Routing/Conditions/PostStatusCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current post's status. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class PostStatusCondition implements ConditionInterface { 20 | /** 21 | * Post status to check against. 22 | * 23 | * @var string 24 | */ 25 | protected $post_status = ''; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @codeCoverageIgnore 31 | * @param string $post_status 32 | */ 33 | public function __construct( $post_status ) { 34 | $this->post_status = $post_status; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function isSatisfied( RequestInterface $request ) { 41 | $post = get_post(); 42 | return ( is_singular() && $post && $this->post_status === $post->post_status ); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getArguments( RequestInterface $request ) { 49 | return ['post_status' => $this->post_status]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Routing/Conditions/PostTemplateCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current post's template. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class PostTemplateCondition implements ConditionInterface { 20 | /** 21 | * Post template to check against 22 | * 23 | * @var string 24 | */ 25 | protected $post_template = ''; 26 | 27 | /** 28 | * Post types to check against 29 | * 30 | * @var string[] 31 | */ 32 | protected $post_types = []; 33 | 34 | /** 35 | * Constructor 36 | * 37 | * @codeCoverageIgnore 38 | * @param string $post_template 39 | * @param string|string[] $post_types 40 | */ 41 | public function __construct( $post_template, $post_types = [] ) { 42 | $this->post_template = $post_template; 43 | $this->post_types = is_array( $post_types ) ? $post_types : [$post_types]; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function isSatisfied( RequestInterface $request ) { 50 | $template = get_post_meta( (int) get_the_ID(), '_wp_page_template', true ); 51 | $template = $template ? $template : 'default'; 52 | return ( is_singular( $this->post_types ) && $this->post_template === $template ); 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function getArguments( RequestInterface $request ) { 59 | return ['post_template' => $this->post_template, 'post_types' => $this->post_types]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Routing/Conditions/PostTypeCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against the current post's type. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class PostTypeCondition implements ConditionInterface { 20 | /** 21 | * Post type to check against 22 | * 23 | * @var string 24 | */ 25 | protected $post_type = ''; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @codeCoverageIgnore 31 | * @param string $post_type 32 | */ 33 | public function __construct( $post_type ) { 34 | $this->post_type = $post_type; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function isSatisfied( RequestInterface $request ) { 41 | return ( is_singular() && $this->post_type === get_post_type() ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function getArguments( RequestInterface $request ) { 48 | return ['post_type' => $this->post_type]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Routing/Conditions/QueryVarCondition.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Check against a query var value. 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | class QueryVarCondition implements ConditionInterface { 20 | /** 21 | * Query var name to check against. 22 | * 23 | * @var string|null 24 | */ 25 | protected $query_var = null; 26 | 27 | /** 28 | * Query var value to check against. 29 | * 30 | * @var string|null 31 | */ 32 | protected $value = ''; 33 | 34 | /** 35 | * Constructor. 36 | * 37 | * @codeCoverageIgnore 38 | * @param string $query_var 39 | * @param string|null $value 40 | */ 41 | public function __construct( $query_var, $value = null ) { 42 | $this->query_var = $query_var; 43 | $this->value = $value; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function isSatisfied( RequestInterface $request ) { 50 | $query_var_value = get_query_var( $this->query_var, null ); 51 | 52 | if ( $query_var_value === null ) { 53 | return false; 54 | } 55 | 56 | if ( $this->value === null ) { 57 | return true; 58 | } 59 | 60 | return (string) $this->value === $query_var_value; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function getArguments( RequestInterface $request ) { 67 | return ['query_var' => $this->query_var, 'value' => $this->value]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Routing/Conditions/UrlableInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing\Conditions; 11 | 12 | /** 13 | * Interface signifying that an object can be converted to a URL. 14 | */ 15 | interface UrlableInterface { 16 | /** 17 | * Convert to URL. 18 | * 19 | * @param array $arguments 20 | * @return string 21 | */ 22 | public function toUrl( $arguments = [] ); 23 | } 24 | -------------------------------------------------------------------------------- /src/Routing/HasQueryFilterInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Requests\RequestInterface; 13 | 14 | /** 15 | * Represent an object which has a WordPress query filter. 16 | */ 17 | interface HasQueryFilterInterface { 18 | /** 19 | * Apply the query filter, if any. 20 | * 21 | * @param RequestInterface $request 22 | * @param array $query_vars 23 | * @return array 24 | */ 25 | public function applyQueryFilter( $request, $query_vars ); 26 | } 27 | -------------------------------------------------------------------------------- /src/Routing/HasQueryFilterTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Exceptions\ConfigurationException; 13 | use WPEmerge\Requests\RequestInterface; 14 | use WPEmerge\Routing\Conditions\CanFilterQueryInterface; 15 | 16 | /** 17 | * Represent an object which has a WordPress query filter attribute. 18 | */ 19 | trait HasQueryFilterTrait { 20 | /** 21 | * Query filter. 22 | * 23 | * @var callable|null 24 | */ 25 | protected $query_filter = null; 26 | 27 | /** 28 | * Get attribute. 29 | * 30 | * @param string $attribute 31 | * @param mixed $default 32 | * @return mixed 33 | */ 34 | abstract public function getAttribute( $attribute, $default = '' ); 35 | 36 | /** 37 | * Apply the query filter, if any. 38 | * 39 | * @param RequestInterface $request 40 | * @param array $query_vars 41 | * @return array 42 | */ 43 | public function applyQueryFilter( $request, $query_vars ) { 44 | $query = $this->getAttribute( 'query' ); 45 | $condition = $this->getAttribute( 'condition' ); 46 | 47 | if ( $query === null ) { 48 | return $query_vars; 49 | } 50 | 51 | if ( ! $condition instanceof CanFilterQueryInterface ) { 52 | throw new ConfigurationException( 53 | 'Only routes with a condition implementing the ' . CanFilterQueryInterface::class . ' ' . 54 | 'interface can apply query filters. ' . 55 | 'Make sure your route has a URL condition and it is not in a non-URL route group.' 56 | ); 57 | } 58 | 59 | return call_user_func_array( 60 | $query, 61 | array_merge( 62 | [$query_vars], 63 | array_values( $condition->getArguments( $request ) ) 64 | ) 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Routing/HasRoutesInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | /** 13 | * Interface for HasRoutesTrait 14 | */ 15 | interface HasRoutesInterface { 16 | /** 17 | * Get routes. 18 | * 19 | * @return RouteInterface[] 20 | */ 21 | public function getRoutes(); 22 | 23 | /** 24 | * Add a route. 25 | * 26 | * @param RouteInterface $route 27 | * @return void 28 | */ 29 | public function addRoute( RouteInterface $route ); 30 | 31 | /** 32 | * Remove a route. 33 | * 34 | * @param RouteInterface $route 35 | * @return void 36 | */ 37 | public function removeRoute( RouteInterface $route ); 38 | } 39 | -------------------------------------------------------------------------------- /src/Routing/HasRoutesTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Exceptions\ConfigurationException; 13 | use WPEmerge\Support\Arr; 14 | 15 | /** 16 | * Allow objects to have routes 17 | */ 18 | trait HasRoutesTrait { 19 | /** 20 | * Array of registered routes 21 | * 22 | * @var RouteInterface[] 23 | */ 24 | protected $routes = []; 25 | 26 | /** 27 | * Get routes. 28 | * 29 | * @codeCoverageIgnore 30 | * @return RouteInterface[] 31 | */ 32 | public function getRoutes() { 33 | return $this->routes; 34 | } 35 | 36 | /** 37 | * Add a route. 38 | * 39 | * @param RouteInterface $route 40 | * @return void 41 | */ 42 | public function addRoute( RouteInterface $route ) { 43 | $routes = $this->getRoutes(); 44 | $name = $route->getAttribute( 'name' ); 45 | 46 | if ( in_array( $route, $routes, true ) ) { 47 | throw new ConfigurationException( 'Attempted to register a route twice.' ); 48 | } 49 | 50 | if ( $name !== '' ) { 51 | foreach ( $routes as $registered ) { 52 | if ( $name === $registered->getAttribute( 'name' ) ) { 53 | throw new ConfigurationException( "The route name \"$name\" is already registered." ); 54 | } 55 | } 56 | } 57 | 58 | $this->routes[] = $route; 59 | } 60 | 61 | /** 62 | * Remove a route. 63 | * 64 | * @param RouteInterface $route 65 | * @return void 66 | */ 67 | public function removeRoute( RouteInterface $route ) { 68 | $routes = $this->getRoutes(); 69 | 70 | $index = array_search( $route, $routes, true ); 71 | 72 | if ( $index === false ) { 73 | return; 74 | } 75 | 76 | $this->routes = array_values( Arr::except( $routes, $index ) ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Routing/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Exceptions\Exception; 13 | 14 | class NotFoundException extends Exception { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Routing/Route.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Exceptions\ConfigurationException; 13 | use WPEmerge\Helpers\HasAttributesTrait; 14 | use WPEmerge\Requests\RequestInterface; 15 | use WPEmerge\Routing\Conditions\ConditionInterface; 16 | 17 | /** 18 | * Represent a route 19 | */ 20 | class Route implements RouteInterface, HasQueryFilterInterface { 21 | use HasAttributesTrait; 22 | use HasQueryFilterTrait; 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function isSatisfied( RequestInterface $request ) { 28 | $methods = $this->getAttribute( 'methods', [] ); 29 | $condition = $this->getAttribute( 'condition' ); 30 | 31 | if ( ! in_array( $request->getMethod(), $methods ) ) { 32 | return false; 33 | } 34 | 35 | if ( ! $condition instanceof ConditionInterface ) { 36 | throw new ConfigurationException( 'Route does not have a condition.' ); 37 | } 38 | 39 | return $condition->isSatisfied( $request ); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getArguments( RequestInterface $request ) { 46 | $condition = $this->getAttribute( 'condition' ); 47 | 48 | if ( ! $condition instanceof ConditionInterface ) { 49 | throw new ConfigurationException( 'Route does not have a condition.' ); 50 | } 51 | 52 | return $condition->getArguments( $request ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Routing/RouteBlueprint.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use Closure; 13 | use WPEmerge\Helpers\HasAttributesTrait; 14 | use WPEmerge\Routing\Conditions\ConditionInterface; 15 | use WPEmerge\View\ViewService; 16 | 17 | /** 18 | * Provide a fluent interface for registering routes with the router. 19 | */ 20 | class RouteBlueprint { 21 | use HasAttributesTrait; 22 | 23 | /** 24 | * Router. 25 | * 26 | * @var Router 27 | */ 28 | protected $router = null; 29 | 30 | /** 31 | * View service. 32 | * 33 | * @var ViewService 34 | */ 35 | protected $view_service = null; 36 | 37 | /** 38 | * Constructor. 39 | * 40 | * @codeCoverageIgnore 41 | * @param Router $router 42 | * @param ViewService $view_service 43 | */ 44 | public function __construct( Router $router, ViewService $view_service ) { 45 | $this->router = $router; 46 | $this->view_service = $view_service; 47 | } 48 | 49 | /** 50 | * Match requests using one of the specified methods. 51 | * 52 | * @param string[] $methods 53 | * @return static $this 54 | */ 55 | public function methods( $methods ) { 56 | $methods = $this->router->mergeMethodsAttribute( 57 | (array) $this->getAttribute( 'methods', [] ), 58 | (array) $methods 59 | ); 60 | 61 | return $this->attribute( 'methods', $methods ); 62 | } 63 | 64 | /** 65 | * Set the condition attribute to a URL. 66 | * 67 | * @param string $url 68 | * @param array $where 69 | * @return static $this 70 | */ 71 | public function url( $url, $where = [] ) { 72 | return $this->where( 'url', $url, $where ); 73 | } 74 | 75 | /** 76 | * Set the condition attribute. 77 | * 78 | * @param string|array|ConditionInterface $condition 79 | * @param mixed ,...$arguments 80 | * @return static $this 81 | */ 82 | public function where( $condition ) { 83 | if ( ! $condition instanceof ConditionInterface ) { 84 | $condition = func_get_args(); 85 | } 86 | 87 | $condition = $this->router->mergeConditionAttribute( 88 | $this->getAttribute( 'condition', null ), 89 | $condition 90 | ); 91 | 92 | return $this->attribute( 'condition', $condition ); 93 | } 94 | 95 | /** 96 | * Set the middleware attribute. 97 | * 98 | * @param string|string[] $middleware 99 | * @return static $this 100 | */ 101 | public function middleware( $middleware ) { 102 | $middleware = $this->router->mergeMiddlewareAttribute( 103 | (array) $this->getAttribute( 'middleware', [] ), 104 | (array) $middleware 105 | ); 106 | 107 | return $this->attribute( 'middleware', $middleware ); 108 | } 109 | 110 | /** 111 | * Set the namespace attribute. 112 | * This should be renamed to namespace for consistency once minimum PHP 113 | * version is increased to 7+. 114 | * 115 | * @param string $namespace 116 | * @return static $this 117 | */ 118 | public function setNamespace( $namespace ) { 119 | $namespace = $this->router->mergeNamespaceAttribute( 120 | $this->getAttribute( 'namespace', '' ), 121 | $namespace 122 | ); 123 | 124 | return $this->attribute( 'namespace', $namespace ); 125 | } 126 | 127 | /** 128 | * Set the query attribute. 129 | * 130 | * @param callable $query 131 | * @return static $this 132 | */ 133 | public function query( $query ) { 134 | $query = $this->router->mergeQueryAttribute( 135 | $this->getAttribute( 'query', null ), 136 | $query 137 | ); 138 | 139 | return $this->attribute( 'query', $query ); 140 | } 141 | 142 | /** 143 | * Set the name attribute. 144 | * 145 | * @param string $name 146 | * @return static $this 147 | */ 148 | public function name( $name ) { 149 | return $this->attribute( 'name', $name ); 150 | } 151 | 152 | /** 153 | * Create a route group. 154 | * 155 | * @param Closure|string $routes Closure or path to file. 156 | * @return void 157 | */ 158 | public function group( $routes ) { 159 | $this->router->group( $this->getAttributes(), $routes ); 160 | } 161 | 162 | /** 163 | * Create a route. 164 | * 165 | * @param string|Closure $handler 166 | * @return void 167 | */ 168 | public function handle( $handler = '' ) { 169 | if ( ! empty( $handler ) ) { 170 | $this->attribute( 'handler', $handler ); 171 | } 172 | 173 | $route = $this->router->route( $this->getAttributes() ); 174 | 175 | $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 ); 176 | 177 | if ( ! empty( $trace ) && ! empty( $trace[0]['file'] ) ) { 178 | $route->attribute( '__definition', $trace[0]['file'] . ':' . $trace[0]['line'] ); 179 | } 180 | 181 | $this->router->addRoute( $route ); 182 | } 183 | 184 | /** 185 | * Handle a request by directly rendering a view. 186 | * 187 | * @param string|string[] $views 188 | * @return void 189 | */ 190 | public function view( $views ) { 191 | $this->handle( function () use ( $views ) { 192 | return $this->view_service->make( $views ); 193 | } ); 194 | } 195 | 196 | /** 197 | * Match ALL requests. 198 | * 199 | * @param string|Closure $handler 200 | * @return void 201 | */ 202 | public function all( $handler = '' ) { 203 | $this->any()->url( '*' )->handle( $handler ); 204 | } 205 | 206 | /** 207 | * Match requests with a method of GET or HEAD. 208 | * 209 | * @return static $this 210 | */ 211 | public function get() { 212 | return $this->methods( ['GET', 'HEAD'] ); 213 | } 214 | 215 | /** 216 | * Match requests with a method of POST. 217 | * 218 | * @return static $this 219 | */ 220 | public function post() { 221 | return $this->methods( ['POST'] ); 222 | } 223 | 224 | /** 225 | * Match requests with a method of PUT. 226 | * 227 | * @return static $this 228 | */ 229 | public function put() { 230 | return $this->methods( ['PUT'] ); 231 | } 232 | 233 | /** 234 | * Match requests with a method of PATCH. 235 | * 236 | * @return static $this 237 | */ 238 | public function patch() { 239 | return $this->methods( ['PATCH'] ); 240 | } 241 | 242 | /** 243 | * Match requests with a method of DELETE. 244 | * 245 | * @return static $this 246 | */ 247 | public function delete() { 248 | return $this->methods( ['DELETE'] ); 249 | } 250 | 251 | /** 252 | * Match requests with a method of OPTIONS. 253 | * 254 | * @return static $this 255 | */ 256 | public function options() { 257 | return $this->methods( ['OPTIONS'] ); 258 | } 259 | 260 | /** 261 | * Match requests with any method. 262 | * 263 | * @return static $this 264 | */ 265 | public function any() { 266 | return $this->methods( ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] ); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Routing/RouteInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use WPEmerge\Helpers\HasAttributesInterface; 13 | use WPEmerge\Requests\RequestInterface; 14 | 15 | /** 16 | * Interface that routes must implement 17 | */ 18 | interface RouteInterface extends HasAttributesInterface { 19 | /** 20 | * Get whether the route is satisfied. 21 | * 22 | * @param RequestInterface $request 23 | * @return boolean 24 | */ 25 | public function isSatisfied( RequestInterface $request ); 26 | 27 | /** 28 | * Get arguments. 29 | * 30 | * @param RequestInterface $request 31 | * @return array 32 | */ 33 | public function getArguments( RequestInterface $request ); 34 | } 35 | -------------------------------------------------------------------------------- /src/Routing/RoutingServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | use Pimple\Container; 13 | use WPEmerge\Routing\Conditions\ConditionFactory; 14 | use WPEmerge\ServiceProviders\ExtendsConfigTrait; 15 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 16 | 17 | /** 18 | * Provide routing dependencies 19 | * 20 | * @codeCoverageIgnore 21 | */ 22 | class RoutingServiceProvider implements ServiceProviderInterface { 23 | use ExtendsConfigTrait; 24 | 25 | /** 26 | * Key=>Class dictionary of condition types 27 | * 28 | * @var array 29 | */ 30 | protected static $condition_types = [ 31 | 'url' => Conditions\UrlCondition::class, 32 | 'custom' => Conditions\CustomCondition::class, 33 | 'multiple' => Conditions\MultipleCondition::class, 34 | 'negate' => Conditions\NegateCondition::class, 35 | 'post_id' => Conditions\PostIdCondition::class, 36 | 'post_slug' => Conditions\PostSlugCondition::class, 37 | 'post_status' => Conditions\PostStatusCondition::class, 38 | 'post_template' => Conditions\PostTemplateCondition::class, 39 | 'post_type' => Conditions\PostTypeCondition::class, 40 | 'query_var' => Conditions\QueryVarCondition::class, 41 | 'ajax' => Conditions\AjaxCondition::class, 42 | 'admin' => Conditions\AdminCondition::class, 43 | ]; 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function register( $container ) { 49 | $namespace = $container[ WPEMERGE_CONFIG_KEY ]['namespace']; 50 | 51 | $this->extendConfig( $container, 'routes', [ 52 | 'web' => [ 53 | 'definitions' => '', 54 | 'attributes' => [ 55 | 'middleware' => ['web'], 56 | 'namespace' => $namespace . 'Controllers\\Web\\', 57 | 'handler' => 'WPEmerge\\Controllers\\WordPressController@handle', 58 | ], 59 | ], 60 | 'admin' => [ 61 | 'definitions' => '', 62 | 'attributes' => [ 63 | 'middleware' => ['admin'], 64 | 'namespace' => $namespace . 'Controllers\\Admin\\', 65 | ], 66 | ], 67 | 'ajax' => [ 68 | 'definitions' => '', 69 | 'attributes' => [ 70 | 'middleware' => ['ajax'], 71 | 'namespace' => $namespace . 'Controllers\\Ajax\\', 72 | ], 73 | ], 74 | ] ); 75 | 76 | /** @var Container $container */ 77 | $container[ WPEMERGE_ROUTING_CONDITION_TYPES_KEY ] = static::$condition_types; 78 | 79 | $container[ WPEMERGE_ROUTING_ROUTER_KEY ] = function ( $c ) { 80 | return new Router( 81 | $c[ WPEMERGE_ROUTING_CONDITIONS_CONDITION_FACTORY_KEY ], 82 | $c[ WPEMERGE_HELPERS_HANDLER_FACTORY_KEY ] 83 | ); 84 | }; 85 | 86 | $container[ WPEMERGE_ROUTING_CONDITIONS_CONDITION_FACTORY_KEY ] = function ( $c ) { 87 | return new ConditionFactory( $c[ WPEMERGE_ROUTING_CONDITION_TYPES_KEY ] ); 88 | }; 89 | 90 | $container[ WPEMERGE_ROUTING_ROUTE_BLUEPRINT_KEY ] = $container->factory( function ( $c ) { 91 | return new RouteBlueprint( $c[ WPEMERGE_ROUTING_ROUTER_KEY ], $c[ WPEMERGE_VIEW_SERVICE_KEY ] ); 92 | } ); 93 | 94 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 95 | $app->alias( 'router', WPEMERGE_ROUTING_ROUTER_KEY ); 96 | $app->alias( 'route', WPEMERGE_ROUTING_ROUTE_BLUEPRINT_KEY ); 97 | $app->alias( 'routeUrl', WPEMERGE_ROUTING_ROUTER_KEY, 'getRouteUrl' ); 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public function bootstrap( $container ) { 104 | // Nothing to bootstrap. 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Routing/SortsMiddlewareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\Routing; 11 | 12 | /** 13 | * Provide middleware sorting. 14 | */ 15 | trait SortsMiddlewareTrait { 16 | /** 17 | * Middleware sorted in order of execution. 18 | * 19 | * @var string[] 20 | */ 21 | protected $middleware_priority = []; 22 | 23 | /** 24 | * Get middleware execution priority. 25 | * 26 | * @codeCoverageIgnore 27 | * @return string[] 28 | */ 29 | public function getMiddlewarePriority() { 30 | return $this->middleware_priority; 31 | } 32 | 33 | /** 34 | * Set middleware execution priority. 35 | * 36 | * @codeCoverageIgnore 37 | * @param string[] $middleware_priority 38 | * @return void 39 | */ 40 | public function setMiddlewarePriority( $middleware_priority ) { 41 | $this->middleware_priority = $middleware_priority; 42 | } 43 | 44 | /** 45 | * Get priority for a specific middleware. 46 | * This is in reverse compared to definition order. 47 | * Middleware with unspecified priority will yield -1. 48 | * 49 | * @param string|array $middleware 50 | * @return integer 51 | */ 52 | public function getMiddlewarePriorityForMiddleware( $middleware ) { 53 | if ( is_array( $middleware ) ) { 54 | $middleware = $middleware[0]; 55 | } 56 | 57 | $increasing_priority = array_reverse( $this->getMiddlewarePriority() ); 58 | $priority = array_search( $middleware, $increasing_priority ); 59 | return $priority !== false ? (int) $priority : -1; 60 | } 61 | 62 | /** 63 | * Sort array of fully qualified middleware class names by priority in ascending order. 64 | * 65 | * @param string[] $middleware 66 | * @return array 67 | */ 68 | public function sortMiddleware( $middleware ) { 69 | $sorted = $middleware; 70 | 71 | usort( $sorted, function ( $a, $b ) use ( $middleware ) { 72 | $a_priority = $this->getMiddlewarePriorityForMiddleware( $a ); 73 | $b_priority = $this->getMiddlewarePriorityForMiddleware( $b ); 74 | $priority = $b_priority - $a_priority; 75 | 76 | if ( $priority !== 0 ) { 77 | return $priority; 78 | } 79 | 80 | // Keep relative order from original array. 81 | return array_search( $a, $middleware ) - array_search( $b, $middleware ); 82 | } ); 83 | 84 | return array_values( $sorted ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ServiceProviders/ExtendsConfigTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\ServiceProviders; 11 | 12 | use Pimple\Container; 13 | use WPEmerge\Support\Arr; 14 | 15 | /** 16 | * Allows objects to extend the config. 17 | */ 18 | trait ExtendsConfigTrait { 19 | /** 20 | * Recursively replace default values with the passed config. 21 | * - If either value is not an array, the config value will be used. 22 | * - If both are an indexed array, the config value will be used. 23 | * - If either is a keyed array, array_replace will be used with config having priority. 24 | * 25 | * @param mixed $default 26 | * @param mixed $config 27 | * @return mixed 28 | */ 29 | protected function replaceConfig( $default, $config ) { 30 | if ( ! is_array( $default ) || ! is_array( $config ) ) { 31 | return $config; 32 | } 33 | 34 | $default_is_indexed = array_keys( $default ) === range( 0, count( $default ) - 1 ); 35 | $config_is_indexed = array_keys( $config ) === range( 0, count( $config ) - 1 ); 36 | 37 | if ( $default_is_indexed && $config_is_indexed ) { 38 | return $config; 39 | } 40 | 41 | $result = $default; 42 | 43 | foreach ( $config as $key => $value ) { 44 | $result[ $key ] = $this->replaceConfig( Arr::get( $default, $key ), $value ); 45 | } 46 | 47 | return $result; 48 | } 49 | 50 | /** 51 | * Extends the WP Emerge config in the container with a new key. 52 | * 53 | * @param Container $container 54 | * @param string $key 55 | * @param mixed $default 56 | * @return void 57 | */ 58 | public function extendConfig( $container, $key, $default ) { 59 | $config = isset( $container[ WPEMERGE_CONFIG_KEY ] ) ? $container[ WPEMERGE_CONFIG_KEY ] : []; 60 | $config = Arr::get( $config, $key, $default ); 61 | 62 | $container[ WPEMERGE_CONFIG_KEY ] = array_merge( 63 | $container[ WPEMERGE_CONFIG_KEY ], 64 | [$key => $this->replaceConfig( $default, $config )] 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ServiceProviders/ServiceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\ServiceProviders; 11 | 12 | use Pimple\Container; 13 | 14 | /** 15 | * Interface that service providers must implement 16 | */ 17 | interface ServiceProviderInterface { 18 | /** 19 | * Register all dependencies in the IoC container. 20 | * 21 | * @param Container $container 22 | * @return void 23 | */ 24 | public function register( $container ); 25 | 26 | /** 27 | * Bootstrap any services if needed. 28 | * 29 | * @param Container $container 30 | * @return void 31 | */ 32 | public function bootstrap( $container ); 33 | } 34 | -------------------------------------------------------------------------------- /src/View/HasContextInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | interface HasContextInterface { 13 | /** 14 | * Get context values. 15 | * 16 | * @param string|null $key 17 | * @param mixed|null $default 18 | * @return mixed 19 | */ 20 | public function getContext( $key = null, $default = null ); 21 | 22 | /** 23 | * Add context values. 24 | * 25 | * @param string|array $key 26 | * @param mixed $value 27 | * @return static $this 28 | */ 29 | public function with( $key, $value = null ); 30 | } 31 | -------------------------------------------------------------------------------- /src/View/HasContextTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Support\Arr; 13 | 14 | trait HasContextTrait { 15 | /** 16 | * Context. 17 | * 18 | * @var array 19 | */ 20 | protected $context = []; 21 | 22 | /** 23 | * Get context values. 24 | * 25 | * @param string|null $key 26 | * @param mixed|null $default 27 | * @return mixed 28 | */ 29 | public function getContext( $key = null, $default = null ) { 30 | if ( $key === null ) { 31 | return $this->context; 32 | } 33 | 34 | return Arr::get( $this->context, $key, $default ); 35 | } 36 | 37 | /** 38 | * Add context values. 39 | * 40 | * @param string|array $key 41 | * @param mixed $value 42 | * @return static $this 43 | */ 44 | public function with( $key, $value = null ) { 45 | if ( is_array( $key ) ) { 46 | $this->context = array_merge( $this->getContext(), $key ); 47 | } else { 48 | $this->context[ $key ] = $value; 49 | } 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/View/HasNameTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | trait HasNameTrait { 13 | /** 14 | * Name. 15 | * 16 | * @var string 17 | */ 18 | protected $name = ''; 19 | 20 | /** 21 | * Get name. 22 | * 23 | * @return string 24 | */ 25 | public function getName() { 26 | return $this->name; 27 | } 28 | 29 | /** 30 | * Set name. 31 | * 32 | * @param string $name 33 | * @return static $this 34 | */ 35 | public function setName( $name ) { 36 | $this->name = $name; 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/View/NameProxyViewEngine.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Application\Application; 13 | 14 | /** 15 | * Render view files with different engines depending on their filename 16 | */ 17 | class NameProxyViewEngine implements ViewEngineInterface { 18 | /** 19 | * Container key of default engine to use 20 | * 21 | * @var string 22 | */ 23 | protected $default = WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY; 24 | 25 | /** 26 | * Application. 27 | * 28 | * @var Application 29 | */ 30 | protected $app = null; 31 | 32 | /** 33 | * Array of filename_suffix=>engine_container_key bindings 34 | * 35 | * @var array 36 | */ 37 | protected $bindings = []; 38 | 39 | /** 40 | * Constructor 41 | * 42 | * @param Application $app 43 | * @param array $bindings 44 | * @param string $default 45 | */ 46 | public function __construct( Application $app, $bindings, $default = '' ) { 47 | $this->app = $app; 48 | $this->bindings = $bindings; 49 | 50 | if ( ! empty( $default ) ) { 51 | $this->default = $default; 52 | } 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function exists( $view ) { 59 | $engine_key = $this->getBindingForFile( $view ); 60 | $engine = $this->app->resolve( $engine_key ); 61 | return $engine->exists( $view ); 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function canonical( $view ) { 68 | $engine_key = $this->getBindingForFile( $view ); 69 | $engine = $this->app->resolve( $engine_key ); 70 | return $engine->canonical( $view ); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | * @throws ViewNotFoundException 76 | */ 77 | public function make( $views ) { 78 | foreach ( $views as $view ) { 79 | if ( $this->exists( $view ) ) { 80 | $engine_key = $this->getBindingForFile( $view ); 81 | $engine = $this->app->resolve( $engine_key ); 82 | return $engine->make( [$view] ); 83 | } 84 | } 85 | 86 | throw new ViewNotFoundException( 'View not found for "' . implode( ', ', $views ) . '"' ); 87 | } 88 | 89 | /** 90 | * Get the default binding 91 | * 92 | * @return string $binding 93 | */ 94 | public function getDefaultBinding() { 95 | return $this->default; 96 | } 97 | 98 | /** 99 | * Get all bindings 100 | * 101 | * @return array $bindings 102 | */ 103 | public function getBindings() { 104 | return $this->bindings; 105 | } 106 | 107 | /** 108 | * Get the engine key binding for a specific file 109 | * 110 | * @param string $file 111 | * @return string 112 | */ 113 | public function getBindingForFile( $file ) { 114 | $engine_key = $this->default; 115 | 116 | foreach ( $this->bindings as $suffix => $engine ) { 117 | if ( substr( $file, -strlen( $suffix ) ) === $suffix ) { 118 | $engine_key = $engine; 119 | break; 120 | } 121 | } 122 | 123 | return $engine_key; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/View/PhpView.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use GuzzleHttp\Psr7; 13 | use GuzzleHttp\Psr7\Response; 14 | 15 | /** 16 | * Render a view file with php. 17 | */ 18 | class PhpView implements ViewInterface { 19 | use HasNameTrait, HasContextTrait; 20 | 21 | /** 22 | * PHP view engine. 23 | * 24 | * @var PhpViewEngine 25 | */ 26 | protected $engine = null; 27 | 28 | /** 29 | * Filepath to view. 30 | * 31 | * @var string 32 | */ 33 | protected $filepath = ''; 34 | 35 | /** 36 | * Layout to use. 37 | * 38 | * @var ViewInterface|null 39 | */ 40 | protected $layout = null; 41 | 42 | /** 43 | * Constructor. 44 | * 45 | * @codeCoverageIgnore 46 | * @param PhpViewEngine $engine 47 | */ 48 | public function __construct( PhpViewEngine $engine ) { 49 | $this->engine = $engine; 50 | } 51 | 52 | /** 53 | * Get filepath. 54 | * 55 | * @return string 56 | */ 57 | public function getFilepath() { 58 | return $this->filepath; 59 | } 60 | 61 | /** 62 | * Set filepath. 63 | * 64 | * @param string $filepath 65 | * @return static $this 66 | */ 67 | public function setFilepath( $filepath ) { 68 | $this->filepath = $filepath; 69 | return $this; 70 | } 71 | 72 | /** 73 | * Get layout. 74 | * 75 | * @return ViewInterface|null 76 | */ 77 | public function getLayout() { 78 | return $this->layout; 79 | } 80 | 81 | /** 82 | * Set layout. 83 | * 84 | * @param ViewInterface|null $layout 85 | * @return static $this 86 | */ 87 | public function setLayout( $layout ) { 88 | $this->layout = $layout; 89 | return $this; 90 | } 91 | 92 | /** 93 | * {@inheritDoc} 94 | * @throws ViewException 95 | */ 96 | public function toString() { 97 | if ( empty( $this->getName() ) ) { 98 | throw new ViewException( 'View must have a name.' ); 99 | } 100 | 101 | if ( empty( $this->getFilepath() ) ) { 102 | throw new ViewException( 'View must have a filepath.' ); 103 | } 104 | 105 | $this->engine->pushLayoutContent( $this ); 106 | 107 | if ( $this->getLayout() !== null ) { 108 | return $this->getLayout()->toString(); 109 | } 110 | 111 | return $this->engine->getLayoutContent(); 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | * @throws ViewException 117 | */ 118 | public function toResponse() { 119 | return (new Response()) 120 | ->withHeader( 'Content-Type', 'text/html' ) 121 | ->withBody( Psr7\stream_for( $this->toString() ) ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/View/PhpViewEngine.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | /** 13 | * Render view files with php. 14 | */ 15 | class PhpViewEngine implements ViewEngineInterface { 16 | /** 17 | * Name of view file header based on which to resolve layouts. 18 | * 19 | * @var string 20 | */ 21 | protected $layout_file_header = 'Layout'; 22 | 23 | /** 24 | * View compose action. 25 | * 26 | * @var callable 27 | */ 28 | protected $compose = null; 29 | 30 | /** 31 | * View finder. 32 | * 33 | * @var PhpViewFilesystemFinder 34 | */ 35 | protected $finder = null; 36 | 37 | /** 38 | * Stack of views ready to be rendered. 39 | * 40 | * @var PhpView[] 41 | */ 42 | protected $layout_content_stack = []; 43 | 44 | /** 45 | * Constructor. 46 | * 47 | * @codeCoverageIgnore 48 | * @param callable $compose 49 | * @param PhpViewFilesystemFinder $finder 50 | */ 51 | public function __construct( callable $compose, PhpViewFilesystemFinder $finder ) { 52 | $this->compose = $compose; 53 | $this->finder = $finder; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function exists( $view ) { 60 | return $this->finder->exists( $view ); 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function canonical( $view ) { 67 | return $this->finder->canonical( $view ); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | * @throws ViewNotFoundException 73 | */ 74 | public function make( $views ) { 75 | foreach ( $views as $view ) { 76 | if ( $this->exists( $view ) ) { 77 | $filepath = $this->finder->resolveFilepath( $view ); 78 | return $this->makeView( $view, $filepath ); 79 | } 80 | } 81 | 82 | throw new ViewNotFoundException( 'View not found for "' . implode( ', ', $views ) . '"' ); 83 | } 84 | 85 | /** 86 | * Create a view instance. 87 | * 88 | * @param string $name 89 | * @param string $filepath 90 | * @return ViewInterface 91 | * @throws ViewNotFoundException 92 | */ 93 | protected function makeView( $name, $filepath ) { 94 | $view = (new PhpView( $this )) 95 | ->setName( $name ) 96 | ->setFilepath( $filepath ); 97 | 98 | $layout = $this->getViewLayout( $view ); 99 | 100 | if ( $layout !== null ) { 101 | $view->setLayout( $layout ); 102 | } 103 | 104 | return $view; 105 | } 106 | 107 | /** 108 | * Create a view instance for the given view's layout header, if any. 109 | * 110 | * @param PhpView $view 111 | * @return ViewInterface|null 112 | * @throws ViewNotFoundException 113 | */ 114 | protected function getViewLayout( PhpView $view ) { 115 | $layout_headers = array_filter( get_file_data( 116 | $view->getFilepath(), 117 | [$this->layout_file_header] 118 | ) ); 119 | 120 | if ( empty( $layout_headers ) ) { 121 | return null; 122 | } 123 | 124 | $layout_file = trim( $layout_headers[0] ); 125 | 126 | if ( ! $this->exists( $layout_file ) ) { 127 | throw new ViewNotFoundException( 'View layout not found for "' . $layout_file . '"' ); 128 | } 129 | 130 | return $this->makeView( $this->canonical( $layout_file ), $this->finder->resolveFilepath( $layout_file ) ); 131 | } 132 | 133 | /** 134 | * Render a view. 135 | * 136 | * @param PhpView $__view 137 | * @return string 138 | */ 139 | protected function renderView( PhpView $__view ) { 140 | $__context = $__view->getContext(); 141 | ob_start(); 142 | extract( $__context, EXTR_OVERWRITE ); 143 | /** @noinspection PhpIncludeInspection */ 144 | include $__view->getFilepath(); 145 | return ob_get_clean(); 146 | } 147 | 148 | /** 149 | * Push layout content to the top of the stack. 150 | * 151 | * @codeCoverageIgnore 152 | * @param PhpView $view 153 | * @return void 154 | */ 155 | public function pushLayoutContent( PhpView $view ) { 156 | $this->layout_content_stack[] = $view; 157 | } 158 | 159 | /** 160 | * Pop the top-most layout content from the stack. 161 | * 162 | * @codeCoverageIgnore 163 | * @return PhpView|null 164 | */ 165 | public function popLayoutContent() { 166 | return array_pop( $this->layout_content_stack ); 167 | } 168 | 169 | /** 170 | * Pop the top-most layout content from the stack, render and return it. 171 | * 172 | * @codeCoverageIgnore 173 | * @return string 174 | */ 175 | public function getLayoutContent() { 176 | $view = $this->popLayoutContent(); 177 | 178 | if ( ! $view ) { 179 | return ''; 180 | } 181 | 182 | $clone = clone $view; 183 | 184 | call_user_func( $this->compose, $clone ); 185 | 186 | return $this->renderView( $clone ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/View/PhpViewFilesystemFinder.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Helpers\MixedType; 13 | 14 | /** 15 | * Render view files with php. 16 | */ 17 | class PhpViewFilesystemFinder implements ViewFinderInterface { 18 | /** 19 | * Custom views directories to check first. 20 | * 21 | * @var string[] 22 | */ 23 | protected $directories = []; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @codeCoverageIgnore 29 | * @param string[] $directories 30 | */ 31 | public function __construct( $directories = [] ) { 32 | $this->setDirectories( $directories ); 33 | } 34 | 35 | /** 36 | * Get the custom views directories. 37 | * 38 | * @codeCoverageIgnore 39 | * @return string[] 40 | */ 41 | public function getDirectories() { 42 | return $this->directories; 43 | } 44 | 45 | /** 46 | * Set the custom views directories. 47 | * 48 | * @codeCoverageIgnore 49 | * @param string[] $directories 50 | * @return void 51 | */ 52 | public function setDirectories( $directories ) { 53 | $this->directories = array_filter( array_map( [MixedType::class, 'removeTrailingSlash'], $directories ) ); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function exists( $view ) { 60 | return ! empty( $this->resolveFilepath( $view ) ); 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function canonical( $view ) { 67 | return $this->resolveFilepath( $view ); 68 | } 69 | 70 | /** 71 | * Resolve a view to an absolute filepath. 72 | * 73 | * @param string $view 74 | * @return string 75 | */ 76 | public function resolveFilepath( $view ) { 77 | $file = $this->resolveFromAbsoluteFilepath( $view ); 78 | 79 | if ( ! $file ) { 80 | $file = $this->resolveFromCustomDirectories( $view ); 81 | } 82 | 83 | return $file; 84 | } 85 | 86 | /** 87 | * Resolve a view if it is a valid absolute filepath. 88 | * 89 | * @param string $view 90 | * @return string 91 | */ 92 | protected function resolveFromAbsoluteFilepath( $view ) { 93 | $path = realpath( MixedType::normalizePath( $view ) ); 94 | 95 | if ( ! empty( $path ) && ! is_file( $path ) ) { 96 | $path = ''; 97 | } 98 | 99 | return $path ? $path : ''; 100 | } 101 | 102 | /** 103 | * Resolve a view if it exists in the custom views directories. 104 | * 105 | * @param string $view 106 | * @return string 107 | */ 108 | protected function resolveFromCustomDirectories( $view ) { 109 | $directories = $this->getDirectories(); 110 | 111 | foreach ( $directories as $directory ) { 112 | $file = MixedType::normalizePath( $directory . DIRECTORY_SEPARATOR . $view ); 113 | 114 | if ( ! is_file( $file ) ) { 115 | // Try adding a .php extension. 116 | $file .= '.php'; 117 | } 118 | 119 | $file = realpath( $file ); 120 | 121 | if ( $file && is_file( $file ) ) { 122 | return $file; 123 | } 124 | } 125 | 126 | return ''; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/View/ViewEngineInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | /** 13 | * Interface that view engines must implement 14 | */ 15 | interface ViewEngineInterface extends ViewFinderInterface { 16 | /** 17 | * Create a view instance from the first view name that exists. 18 | * 19 | * @param string[] $views 20 | * @return ViewInterface 21 | */ 22 | public function make( $views ); 23 | } 24 | -------------------------------------------------------------------------------- /src/View/ViewException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Exceptions\Exception; 13 | 14 | class ViewException extends Exception { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/View/ViewFinderInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | /** 13 | * Interface that view finders must implement. 14 | */ 15 | interface ViewFinderInterface { 16 | /** 17 | * Check if a view exists. 18 | * 19 | * @param string $view 20 | * @return boolean 21 | */ 22 | public function exists( $view ); 23 | 24 | /** 25 | * Return a canonical string representation of the view name. 26 | * 27 | * @param string $view 28 | * @return string 29 | */ 30 | public function canonical( $view ); 31 | } 32 | -------------------------------------------------------------------------------- /src/View/ViewInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Responses\ResponsableInterface; 13 | 14 | /** 15 | * Represent and render a view to a string. 16 | */ 17 | interface ViewInterface extends HasContextInterface, ResponsableInterface { 18 | /** 19 | * Get name. 20 | * 21 | * @return string 22 | */ 23 | public function getName(); 24 | 25 | /** 26 | * Set name. 27 | * 28 | * @param string $name 29 | * @return static $this 30 | */ 31 | public function setName( $name ); 32 | 33 | /** 34 | * Render the view to a string. 35 | * 36 | * @return string 37 | */ 38 | public function toString(); 39 | } 40 | -------------------------------------------------------------------------------- /src/View/ViewNotFoundException.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use WPEmerge\Exceptions\Exception; 13 | 14 | class ViewNotFoundException extends Exception { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/View/ViewService.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use Closure; 13 | use WPEmerge\Helpers\Handler; 14 | use WPEmerge\Helpers\HandlerFactory; 15 | use WPEmerge\Helpers\MixedType; 16 | 17 | /** 18 | * Provide general view-related functionality. 19 | */ 20 | class ViewService { 21 | /** 22 | * Configuration. 23 | * 24 | * @var array 25 | */ 26 | protected $config = []; 27 | 28 | /** 29 | * View engine. 30 | * 31 | * @var ViewEngineInterface 32 | */ 33 | protected $engine = null; 34 | 35 | /** 36 | * Handler factory. 37 | * 38 | * @var HandlerFactory 39 | */ 40 | protected $handler_factory = null; 41 | 42 | /** 43 | * Global variables. 44 | * 45 | * @var array 46 | */ 47 | protected $globals = []; 48 | 49 | /** 50 | * View composers. 51 | * 52 | * @var array 53 | */ 54 | protected $composers = []; 55 | 56 | /** 57 | * Constructor. 58 | * 59 | * @codeCoverageIgnore 60 | * @param array $config 61 | * @param ViewEngineInterface $engine 62 | * @param HandlerFactory $handler_factory 63 | */ 64 | public function __construct( $config, ViewEngineInterface $engine, HandlerFactory $handler_factory ) { 65 | $this->config = $config; 66 | $this->engine = $engine; 67 | $this->handler_factory = $handler_factory; 68 | } 69 | 70 | /** 71 | * Get global variables. 72 | * 73 | * @return array 74 | */ 75 | public function getGlobals() { 76 | return $this->globals; 77 | } 78 | 79 | /** 80 | * Set a global variable. 81 | * 82 | * @param string $key 83 | * @param mixed $value 84 | * @return void 85 | */ 86 | public function addGlobal( $key, $value ) { 87 | $this->globals[ $key ] = $value; 88 | } 89 | 90 | /** 91 | * Set an array of global variables. 92 | * 93 | * @param array $globals 94 | * @return void 95 | */ 96 | public function addGlobals( $globals ) { 97 | foreach ( $globals as $key => $value ) { 98 | $this->addGlobal( $key, $value ); 99 | } 100 | } 101 | 102 | /** 103 | * Get view composer. 104 | * 105 | * @param string $view 106 | * @return Handler[] 107 | */ 108 | public function getComposersForView( $view ) { 109 | $view = $this->engine->canonical( $view ); 110 | 111 | $composers = []; 112 | 113 | foreach ( $this->composers as $composer ) { 114 | if ( in_array( $view, $composer['views'], true ) ) { 115 | $composers[] = $composer['composer']; 116 | } 117 | } 118 | 119 | return $composers; 120 | } 121 | 122 | /** 123 | * Add view composer. 124 | * 125 | * @param string|string[] $views 126 | * @param string|Closure $composer 127 | * @return void 128 | */ 129 | public function addComposer( $views, $composer ) { 130 | $views = array_map( function ( $view ) { 131 | return $this->engine->canonical( $view ); 132 | }, MixedType::toArray( $views ) ); 133 | 134 | $handler = $this->handler_factory->make( $composer, 'compose', $this->config['namespace'] ); 135 | 136 | $this->composers[] = [ 137 | 'views' => $views, 138 | 'composer' => $handler, 139 | ]; 140 | } 141 | 142 | /** 143 | * Composes a view instance with contexts in the following order: Global, Composers, Local. 144 | * 145 | * @param ViewInterface $view 146 | * @return void 147 | */ 148 | public function compose( ViewInterface $view ) { 149 | $global = ['global' => $this->getGlobals()]; 150 | $local = $view->getContext(); 151 | 152 | $view->with( $global ); 153 | 154 | $composers = $this->getComposersForView( $view->getName() ); 155 | foreach ( $composers as $composer ) { 156 | $composer->execute( $view ); 157 | } 158 | 159 | $view->with( $local ); 160 | } 161 | 162 | /** 163 | * Check if a view exists. 164 | * 165 | * @param string $view 166 | * @return boolean 167 | */ 168 | public function exists( $view ) { 169 | return $this->engine->exists( $view ); 170 | } 171 | 172 | /** 173 | * Return a canonical string representation of the view name. 174 | * 175 | * @param string $view 176 | * @return string 177 | */ 178 | public function canonical( $view ) { 179 | return $this->engine->canonical( $view ); 180 | } 181 | 182 | /** 183 | * Create a view instance from the first view name that exists. 184 | * 185 | * @param string|string[] $views 186 | * @return ViewInterface 187 | */ 188 | public function make( $views ) { 189 | return $this->engine->make( MixedType::toArray( $views ) ); 190 | } 191 | 192 | /** 193 | * Trigger core hooks for a partial, if any. 194 | * 195 | * @codeCoverageIgnore 196 | * @param string $name 197 | * @return void 198 | */ 199 | public function triggerPartialHooks( $name ) { 200 | if ( ! function_exists( 'apply_filters' ) ) { 201 | // We are not in a WordPress environment - skip triggering hooks. 202 | return; 203 | } 204 | 205 | $core_partial = '/^(header|sidebar|footer)(?:-(.*?))?(\.|$)/i'; 206 | $matches = []; 207 | $is_partial = preg_match( $core_partial, $name, $matches ); 208 | 209 | if ( $is_partial && apply_filters( "wpemerge.partials.{$matches[1]}.hook", true ) ) { 210 | do_action( "get_{$matches[1]}", $matches[2] ); 211 | } 212 | } 213 | 214 | /** 215 | * Render a view. 216 | * 217 | * @codeCoverageIgnore 218 | * @param string|string[] $views 219 | * @param array $context 220 | * @return void 221 | */ 222 | public function render( $views, $context = [] ) { 223 | $view = $this->make( $views )->with( $context ); 224 | $this->triggerPartialHooks( $view->getName() ); 225 | echo $view->toString(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/View/ViewServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | namespace WPEmerge\View; 11 | 12 | use Pimple\Container; 13 | use WPEmerge\Helpers\MixedType; 14 | use WPEmerge\ServiceProviders\ExtendsConfigTrait; 15 | use WPEmerge\ServiceProviders\ServiceProviderInterface; 16 | 17 | /** 18 | * Provide view dependencies 19 | * 20 | * @codeCoverageIgnore 21 | */ 22 | class ViewServiceProvider implements ServiceProviderInterface { 23 | use ExtendsConfigTrait; 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function register( $container ) { 29 | /** @var Container $container */ 30 | $namespace = $container[ WPEMERGE_CONFIG_KEY ]['namespace']; 31 | 32 | $this->extendConfig( $container, 'views', [get_stylesheet_directory(), get_template_directory()] ); 33 | 34 | $this->extendConfig( $container, 'view_composers', [ 35 | 'namespace' => $namespace . 'ViewComposers\\', 36 | ] ); 37 | 38 | $container[ WPEMERGE_VIEW_SERVICE_KEY ] = function ( $c ) { 39 | return new ViewService( 40 | $c[ WPEMERGE_CONFIG_KEY ]['view_composers'], 41 | $c[ WPEMERGE_VIEW_ENGINE_KEY ], 42 | $c[ WPEMERGE_HELPERS_HANDLER_FACTORY_KEY ] 43 | ); 44 | }; 45 | 46 | $container[ WPEMERGE_VIEW_COMPOSE_ACTION_KEY ] = function ( $c ) { 47 | return function ( ViewInterface $view ) use ( $c ) { 48 | $view_service = $c[ WPEMERGE_VIEW_SERVICE_KEY ]; 49 | $view_service->compose( $view ); 50 | return $view; 51 | }; 52 | }; 53 | 54 | $container[ WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY ] = function ( $c ) { 55 | $finder = new PhpViewFilesystemFinder( MixedType::toArray( $c[ WPEMERGE_CONFIG_KEY ]['views'] ) ); 56 | return new PhpViewEngine( $c[ WPEMERGE_VIEW_COMPOSE_ACTION_KEY ], $finder ); 57 | }; 58 | 59 | $container[ WPEMERGE_VIEW_ENGINE_KEY ] = function ( $c ) { 60 | return $c[ WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY ]; 61 | }; 62 | 63 | $app = $container[ WPEMERGE_APPLICATION_KEY ]; 64 | $app->alias( 'views', WPEMERGE_VIEW_SERVICE_KEY ); 65 | 66 | $app->alias( 'view', function () use ( $app ) { 67 | return call_user_func_array( [$app->views(), 'make'], func_get_args() ); 68 | } ); 69 | 70 | $app->alias( 'render', function () use ( $app ) { 71 | return call_user_func_array( [$app->views(), 'render'], func_get_args() ); 72 | } ); 73 | 74 | $app->alias( 'layoutContent', function () use ( $app ) { 75 | /** @var PhpViewEngine $engine */ 76 | $engine = $app->resolve( WPEMERGE_VIEW_PHP_VIEW_ENGINE_KEY ); 77 | 78 | echo $engine->getLayoutContent(); 79 | } ); 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function bootstrap( $container ) { 86 | // Nothing to bootstrap. 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/view.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2017-2019 Atanas Angelov 6 | * @license https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0 7 | * @link https://wpemerge.com/ 8 | */ 9 | 10 | if ( ! defined( 'ABSPATH' ) ) { 11 | exit; 12 | } 13 | 14 | do_action( 'wpemerge.kernels.http_kernel.respond' ); 15 | remove_all_filters( 'wpemerge.kernels.http_kernel.respond' ); 16 | --------------------------------------------------------------------------------